123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711 |
- # Copyright (c) 2012 Google Inc. All rights reserved.
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- import errno
- import filecmp
- import os.path
- import re
- import tempfile
- import sys
- import subprocess
- import shlex
- from collections.abc import MutableSet
- # A minimal memoizing decorator. It'll blow up if the args aren't immutable,
- # among other "problems".
- class memoize:
- def __init__(self, func):
- self.func = func
- self.cache = {}
- def __call__(self, *args):
- try:
- return self.cache[args]
- except KeyError:
- result = self.func(*args)
- self.cache[args] = result
- return result
- class GypError(Exception):
- """Error class representing an error, which is to be presented
- to the user. The main entry point will catch and display this.
- """
- pass
- def ExceptionAppend(e, msg):
- """Append a message to the given exception's message."""
- if not e.args:
- e.args = (msg,)
- elif len(e.args) == 1:
- e.args = (str(e.args[0]) + " " + msg,)
- else:
- e.args = (str(e.args[0]) + " " + msg,) + e.args[1:]
- def FindQualifiedTargets(target, qualified_list):
- """
- Given a list of qualified targets, return the qualified targets for the
- specified |target|.
- """
- return [t for t in qualified_list if ParseQualifiedTarget(t)[1] == target]
- def ParseQualifiedTarget(target):
- # Splits a qualified target into a build file, target name and toolset.
- # NOTE: rsplit is used to disambiguate the Windows drive letter separator.
- target_split = target.rsplit(":", 1)
- if len(target_split) == 2:
- [build_file, target] = target_split
- else:
- build_file = None
- target_split = target.rsplit("#", 1)
- if len(target_split) == 2:
- [target, toolset] = target_split
- else:
- toolset = None
- return [build_file, target, toolset]
- def ResolveTarget(build_file, target, toolset):
- # This function resolves a target into a canonical form:
- # - a fully defined build file, either absolute or relative to the current
- # directory
- # - a target name
- # - a toolset
- #
- # build_file is the file relative to which 'target' is defined.
- # target is the qualified target.
- # toolset is the default toolset for that target.
- [parsed_build_file, target, parsed_toolset] = ParseQualifiedTarget(target)
- if parsed_build_file:
- if build_file:
- # If a relative path, parsed_build_file is relative to the directory
- # containing build_file. If build_file is not in the current directory,
- # parsed_build_file is not a usable path as-is. Resolve it by
- # interpreting it as relative to build_file. If parsed_build_file is
- # absolute, it is usable as a path regardless of the current directory,
- # and os.path.join will return it as-is.
- build_file = os.path.normpath(
- os.path.join(os.path.dirname(build_file), parsed_build_file)
- )
- # Further (to handle cases like ../cwd), make it relative to cwd)
- if not os.path.isabs(build_file):
- build_file = RelativePath(build_file, ".")
- else:
- build_file = parsed_build_file
- if parsed_toolset:
- toolset = parsed_toolset
- return [build_file, target, toolset]
- def BuildFile(fully_qualified_target):
- # Extracts the build file from the fully qualified target.
- return ParseQualifiedTarget(fully_qualified_target)[0]
- def GetEnvironFallback(var_list, default):
- """Look up a key in the environment, with fallback to secondary keys
- and finally falling back to a default value."""
- for var in var_list:
- if var in os.environ:
- return os.environ[var]
- return default
- def QualifiedTarget(build_file, target, toolset):
- # "Qualified" means the file that a target was defined in and the target
- # name, separated by a colon, suffixed by a # and the toolset name:
- # /path/to/file.gyp:target_name#toolset
- fully_qualified = build_file + ":" + target
- if toolset:
- fully_qualified = fully_qualified + "#" + toolset
- return fully_qualified
- @memoize
- def RelativePath(path, relative_to, follow_path_symlink=True):
- # Assuming both |path| and |relative_to| are relative to the current
- # directory, returns a relative path that identifies path relative to
- # relative_to.
- # If |follow_symlink_path| is true (default) and |path| is a symlink, then
- # this method returns a path to the real file represented by |path|. If it is
- # false, this method returns a path to the symlink. If |path| is not a
- # symlink, this option has no effect.
- # Convert to normalized (and therefore absolute paths).
- path = os.path.realpath(path) if follow_path_symlink else os.path.abspath(path)
- relative_to = os.path.realpath(relative_to)
- # On Windows, we can't create a relative path to a different drive, so just
- # use the absolute path.
- if sys.platform == "win32" and (
- os.path.splitdrive(path)[0].lower()
- != os.path.splitdrive(relative_to)[0].lower()
- ):
- return path
- # Split the paths into components.
- path_split = path.split(os.path.sep)
- relative_to_split = relative_to.split(os.path.sep)
- # Determine how much of the prefix the two paths share.
- prefix_len = len(os.path.commonprefix([path_split, relative_to_split]))
- # Put enough ".." components to back up out of relative_to to the common
- # prefix, and then append the part of path_split after the common prefix.
- relative_split = [os.path.pardir] * (
- len(relative_to_split) - prefix_len
- ) + path_split[prefix_len:]
- if len(relative_split) == 0:
- # The paths were the same.
- return ""
- # Turn it back into a string and we're done.
- return os.path.join(*relative_split)
- @memoize
- def InvertRelativePath(path, toplevel_dir=None):
- """Given a path like foo/bar that is relative to toplevel_dir, return
- the inverse relative path back to the toplevel_dir.
- E.g. os.path.normpath(os.path.join(path, InvertRelativePath(path)))
- should always produce the empty string, unless the path contains symlinks.
- """
- if not path:
- return path
- toplevel_dir = "." if toplevel_dir is None else toplevel_dir
- return RelativePath(toplevel_dir, os.path.join(toplevel_dir, path))
- def FixIfRelativePath(path, relative_to):
- # Like RelativePath but returns |path| unchanged if it is absolute.
- if os.path.isabs(path):
- return path
- return RelativePath(path, relative_to)
- def UnrelativePath(path, relative_to):
- # Assuming that |relative_to| is relative to the current directory, and |path|
- # is a path relative to the dirname of |relative_to|, returns a path that
- # identifies |path| relative to the current directory.
- rel_dir = os.path.dirname(relative_to)
- return os.path.normpath(os.path.join(rel_dir, path))
- # re objects used by EncodePOSIXShellArgument. See IEEE 1003.1 XCU.2.2 at
- # http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_02
- # and the documentation for various shells.
- # _quote is a pattern that should match any argument that needs to be quoted
- # with double-quotes by EncodePOSIXShellArgument. It matches the following
- # characters appearing anywhere in an argument:
- # \t, \n, space parameter separators
- # # comments
- # $ expansions (quoted to always expand within one argument)
- # % called out by IEEE 1003.1 XCU.2.2
- # & job control
- # ' quoting
- # (, ) subshell execution
- # *, ?, [ pathname expansion
- # ; command delimiter
- # <, >, | redirection
- # = assignment
- # {, } brace expansion (bash)
- # ~ tilde expansion
- # It also matches the empty string, because "" (or '') is the only way to
- # represent an empty string literal argument to a POSIX shell.
- #
- # This does not match the characters in _escape, because those need to be
- # backslash-escaped regardless of whether they appear in a double-quoted
- # string.
- _quote = re.compile("[\t\n #$%&'()*;<=>?[{|}~]|^$")
- # _escape is a pattern that should match any character that needs to be
- # escaped with a backslash, whether or not the argument matched the _quote
- # pattern. _escape is used with re.sub to backslash anything in _escape's
- # first match group, hence the (parentheses) in the regular expression.
- #
- # _escape matches the following characters appearing anywhere in an argument:
- # " to prevent POSIX shells from interpreting this character for quoting
- # \ to prevent POSIX shells from interpreting this character for escaping
- # ` to prevent POSIX shells from interpreting this character for command
- # substitution
- # Missing from this list is $, because the desired behavior of
- # EncodePOSIXShellArgument is to permit parameter (variable) expansion.
- #
- # Also missing from this list is !, which bash will interpret as the history
- # expansion character when history is enabled. bash does not enable history
- # by default in non-interactive shells, so this is not thought to be a problem.
- # ! was omitted from this list because bash interprets "\!" as a literal string
- # including the backslash character (avoiding history expansion but retaining
- # the backslash), which would not be correct for argument encoding. Handling
- # this case properly would also be problematic because bash allows the history
- # character to be changed with the histchars shell variable. Fortunately,
- # as history is not enabled in non-interactive shells and
- # EncodePOSIXShellArgument is only expected to encode for non-interactive
- # shells, there is no room for error here by ignoring !.
- _escape = re.compile(r'(["\\`])')
- def EncodePOSIXShellArgument(argument):
- """Encodes |argument| suitably for consumption by POSIX shells.
- argument may be quoted and escaped as necessary to ensure that POSIX shells
- treat the returned value as a literal representing the argument passed to
- this function. Parameter (variable) expansions beginning with $ are allowed
- to remain intact without escaping the $, to allow the argument to contain
- references to variables to be expanded by the shell.
- """
- if not isinstance(argument, str):
- argument = str(argument)
- quote = '"' if _quote.search(argument) else ""
- encoded = quote + re.sub(_escape, r"\\\1", argument) + quote
- return encoded
- def EncodePOSIXShellList(list):
- """Encodes |list| suitably for consumption by POSIX shells.
- Returns EncodePOSIXShellArgument for each item in list, and joins them
- together using the space character as an argument separator.
- """
- encoded_arguments = []
- for argument in list:
- encoded_arguments.append(EncodePOSIXShellArgument(argument))
- return " ".join(encoded_arguments)
- def DeepDependencyTargets(target_dicts, roots):
- """Returns the recursive list of target dependencies."""
- dependencies = set()
- pending = set(roots)
- while pending:
- # Pluck out one.
- r = pending.pop()
- # Skip if visited already.
- if r in dependencies:
- continue
- # Add it.
- dependencies.add(r)
- # Add its children.
- spec = target_dicts[r]
- pending.update(set(spec.get("dependencies", [])))
- pending.update(set(spec.get("dependencies_original", [])))
- return list(dependencies - set(roots))
- def BuildFileTargets(target_list, build_file):
- """From a target_list, returns the subset from the specified build_file.
- """
- return [p for p in target_list if BuildFile(p) == build_file]
- def AllTargets(target_list, target_dicts, build_file):
- """Returns all targets (direct and dependencies) for the specified build_file.
- """
- bftargets = BuildFileTargets(target_list, build_file)
- deptargets = DeepDependencyTargets(target_dicts, bftargets)
- return bftargets + deptargets
- def WriteOnDiff(filename):
- """Write to a file only if the new contents differ.
- Arguments:
- filename: name of the file to potentially write to.
- Returns:
- A file like object which will write to temporary file and only overwrite
- the target if it differs (on close).
- """
- class Writer:
- """Wrapper around file which only covers the target if it differs."""
- def __init__(self):
- # On Cygwin remove the "dir" argument
- # `C:` prefixed paths are treated as relative,
- # consequently ending up with current dir "/cygdrive/c/..."
- # being prefixed to those, which was
- # obviously a non-existent path,
- # for example: "/cygdrive/c/<some folder>/C:\<my win style abs path>".
- # For more details see:
- # https://docs.python.org/2/library/tempfile.html#tempfile.mkstemp
- base_temp_dir = "" if IsCygwin() else os.path.dirname(filename)
- # Pick temporary file.
- tmp_fd, self.tmp_path = tempfile.mkstemp(
- suffix=".tmp",
- prefix=os.path.split(filename)[1] + ".gyp.",
- dir=base_temp_dir,
- )
- try:
- self.tmp_file = os.fdopen(tmp_fd, "wb")
- except Exception:
- # Don't leave turds behind.
- os.unlink(self.tmp_path)
- raise
- def __getattr__(self, attrname):
- # Delegate everything else to self.tmp_file
- return getattr(self.tmp_file, attrname)
- def close(self):
- try:
- # Close tmp file.
- self.tmp_file.close()
- # Determine if different.
- same = False
- try:
- same = filecmp.cmp(self.tmp_path, filename, False)
- except OSError as e:
- if e.errno != errno.ENOENT:
- raise
- if same:
- # The new file is identical to the old one, just get rid of the new
- # one.
- os.unlink(self.tmp_path)
- else:
- # The new file is different from the old one,
- # or there is no old one.
- # Rename the new file to the permanent name.
- #
- # tempfile.mkstemp uses an overly restrictive mode, resulting in a
- # file that can only be read by the owner, regardless of the umask.
- # There's no reason to not respect the umask here,
- # which means that an extra hoop is required
- # to fetch it and reset the new file's mode.
- #
- # No way to get the umask without setting a new one? Set a safe one
- # and then set it back to the old value.
- umask = os.umask(0o77)
- os.umask(umask)
- os.chmod(self.tmp_path, 0o666 & ~umask)
- if sys.platform == "win32" and os.path.exists(filename):
- # NOTE: on windows (but not cygwin) rename will not replace an
- # existing file, so it must be preceded with a remove.
- # Sadly there is no way to make the switch atomic.
- os.remove(filename)
- os.rename(self.tmp_path, filename)
- except Exception:
- # Don't leave turds behind.
- os.unlink(self.tmp_path)
- raise
- def write(self, s):
- self.tmp_file.write(s.encode("utf-8"))
- return Writer()
- def EnsureDirExists(path):
- """Make sure the directory for |path| exists."""
- try:
- os.makedirs(os.path.dirname(path))
- except OSError:
- pass
- def GetCrossCompilerPredefines(): # -> dict
- cmd = []
- # shlex.split() will eat '\' in posix mode, but
- # setting posix=False will preserve extra '"' cause CreateProcess fail on Windows
- # this makes '\' in %CC_target% and %CFLAGS% work
- def replace_sep(s):
- return s.replace(os.sep, "/") if os.sep != "/" else s
- if CC := os.environ.get("CC_target") or os.environ.get("CC"):
- cmd += shlex.split(replace_sep(CC))
- if CFLAGS := os.environ.get("CFLAGS"):
- cmd += shlex.split(replace_sep(CFLAGS))
- elif CXX := os.environ.get("CXX_target") or os.environ.get("CXX"):
- cmd += shlex.split(replace_sep(CXX))
- if CXXFLAGS := os.environ.get("CXXFLAGS"):
- cmd += shlex.split(replace_sep(CXXFLAGS))
- else:
- return {}
- if sys.platform == "win32":
- fd, input = tempfile.mkstemp(suffix=".c")
- real_cmd = [*cmd, "-dM", "-E", "-x", "c", input]
- try:
- os.close(fd)
- stdout = subprocess.run(
- real_cmd, shell=True,
- capture_output=True, check=True
- ).stdout
- finally:
- os.unlink(input)
- else:
- input = "/dev/null"
- real_cmd = [*cmd, "-dM", "-E", "-x", "c", input]
- stdout = subprocess.run(
- real_cmd, shell=False,
- capture_output=True, check=True
- ).stdout
- defines = {}
- lines = stdout.decode("utf-8").replace("\r\n", "\n").split("\n")
- for line in lines:
- if (line or "").startswith("#define "):
- _, key, *value = line.split(" ")
- defines[key] = " ".join(value)
- return defines
- def GetFlavorByPlatform():
- """Returns |params.flavor| if it's set, the system's default flavor else."""
- flavors = {
- "cygwin": "win",
- "win32": "win",
- "darwin": "mac",
- }
- if sys.platform in flavors:
- return flavors[sys.platform]
- if sys.platform.startswith("sunos"):
- return "solaris"
- if sys.platform.startswith(("dragonfly", "freebsd")):
- return "freebsd"
- if sys.platform.startswith("openbsd"):
- return "openbsd"
- if sys.platform.startswith("netbsd"):
- return "netbsd"
- if sys.platform.startswith("aix"):
- return "aix"
- if sys.platform.startswith(("os390", "zos")):
- return "zos"
- if sys.platform == "os400":
- return "os400"
- return "linux"
- def GetFlavor(params):
- if "flavor" in params:
- return params["flavor"]
- defines = GetCrossCompilerPredefines()
- if "__EMSCRIPTEN__" in defines:
- return "emscripten"
- if "__wasm__" in defines:
- return "wasi" if "__wasi__" in defines else "wasm"
- return GetFlavorByPlatform()
- def CopyTool(flavor, out_path, generator_flags={}):
- """Finds (flock|mac|win)_tool.gyp in the gyp directory and copies it
- to |out_path|."""
- # aix and solaris just need flock emulation. mac and win use more complicated
- # support scripts.
- prefix = {
- "aix": "flock",
- "os400": "flock",
- "solaris": "flock",
- "mac": "mac",
- "ios": "mac",
- "win": "win",
- }.get(flavor, None)
- if not prefix:
- return
- # Slurp input file.
- source_path = os.path.join(
- os.path.dirname(os.path.abspath(__file__)), "%s_tool.py" % prefix
- )
- with open(source_path) as source_file:
- source = source_file.readlines()
- # Set custom header flags.
- header = "# Generated by gyp. Do not edit.\n"
- mac_toolchain_dir = generator_flags.get("mac_toolchain_dir", None)
- if flavor == "mac" and mac_toolchain_dir:
- header += "import os;\nos.environ['DEVELOPER_DIR']='%s'\n" % mac_toolchain_dir
- # Add header and write it out.
- tool_path = os.path.join(out_path, "gyp-%s-tool" % prefix)
- with open(tool_path, "w") as tool_file:
- tool_file.write("".join([source[0], header] + source[1:]))
- # Make file executable.
- os.chmod(tool_path, 0o755)
- # From Alex Martelli,
- # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52560
- # ASPN: Python Cookbook: Remove duplicates from a sequence
- # First comment, dated 2001/10/13.
- # (Also in the printed Python Cookbook.)
- def uniquer(seq, idfun=lambda x: x):
- seen = {}
- result = []
- for item in seq:
- marker = idfun(item)
- if marker in seen:
- continue
- seen[marker] = 1
- result.append(item)
- return result
- # Based on http://code.activestate.com/recipes/576694/.
- class OrderedSet(MutableSet):
- def __init__(self, iterable=None):
- self.end = end = []
- end += [None, end, end] # sentinel node for doubly linked list
- self.map = {} # key --> [key, prev, next]
- if iterable is not None:
- self |= iterable
- def __len__(self):
- return len(self.map)
- def __contains__(self, key):
- return key in self.map
- def add(self, key):
- if key not in self.map:
- end = self.end
- curr = end[1]
- curr[2] = end[1] = self.map[key] = [key, curr, end]
- def discard(self, key):
- if key in self.map:
- key, prev_item, next_item = self.map.pop(key)
- prev_item[2] = next_item
- next_item[1] = prev_item
- def __iter__(self):
- end = self.end
- curr = end[2]
- while curr is not end:
- yield curr[0]
- curr = curr[2]
- def __reversed__(self):
- end = self.end
- curr = end[1]
- while curr is not end:
- yield curr[0]
- curr = curr[1]
- # The second argument is an addition that causes a pylint warning.
- def pop(self, last=True): # pylint: disable=W0221
- if not self:
- raise KeyError("set is empty")
- key = self.end[1][0] if last else self.end[2][0]
- self.discard(key)
- return key
- def __repr__(self):
- if not self:
- return f"{self.__class__.__name__}()"
- return f"{self.__class__.__name__}({list(self)!r})"
- def __eq__(self, other):
- if isinstance(other, OrderedSet):
- return len(self) == len(other) and list(self) == list(other)
- return set(self) == set(other)
- # Extensions to the recipe.
- def update(self, iterable):
- for i in iterable:
- if i not in self:
- self.add(i)
- class CycleError(Exception):
- """An exception raised when an unexpected cycle is detected."""
- def __init__(self, nodes):
- self.nodes = nodes
- def __str__(self):
- return "CycleError: cycle involving: " + str(self.nodes)
- def TopologicallySorted(graph, get_edges):
- r"""Topologically sort based on a user provided edge definition.
- Args:
- graph: A list of node names.
- get_edges: A function mapping from node name to a hashable collection
- of node names which this node has outgoing edges to.
- Returns:
- A list containing all of the node in graph in topological order.
- It is assumed that calling get_edges once for each node and caching is
- cheaper than repeatedly calling get_edges.
- Raises:
- CycleError in the event of a cycle.
- Example:
- graph = {'a': '$(b) $(c)', 'b': 'hi', 'c': '$(b)'}
- def GetEdges(node):
- return re.findall(r'\$\(([^))]\)', graph[node])
- print TopologicallySorted(graph.keys(), GetEdges)
- ==>
- ['a', 'c', b']
- """
- get_edges = memoize(get_edges)
- visited = set()
- visiting = set()
- ordered_nodes = []
- def Visit(node):
- if node in visiting:
- raise CycleError(visiting)
- if node in visited:
- return
- visited.add(node)
- visiting.add(node)
- for neighbor in get_edges(node):
- Visit(neighbor)
- visiting.remove(node)
- ordered_nodes.insert(0, node)
- for node in sorted(graph):
- Visit(node)
- return ordered_nodes
- def CrossCompileRequested():
- # TODO: figure out how to not build extra host objects in the
- # non-cross-compile case when this is enabled, and enable unconditionally.
- return (
- os.environ.get("GYP_CROSSCOMPILE")
- or os.environ.get("AR_host")
- or os.environ.get("CC_host")
- or os.environ.get("CXX_host")
- or os.environ.get("AR_target")
- or os.environ.get("CC_target")
- or os.environ.get("CXX_target")
- )
- def IsCygwin():
- try:
- out = subprocess.Popen(
- "uname", stdout=subprocess.PIPE, stderr=subprocess.STDOUT
- )
- stdout = out.communicate()[0].decode("utf-8")
- return "CYGWIN" in str(stdout)
- except Exception:
- return False
|