diff --git a/build/fbcode_builder/getdeps/envfuncs.py b/build/fbcode_builder/getdeps/envfuncs.py new file mode 100644 index 000000000..737132e48 --- /dev/null +++ b/build/fbcode_builder/getdeps/envfuncs.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# Copyright (c) 2019-present, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. + +from __future__ import absolute_import, division, print_function, unicode_literals + +import os +import shlex +import sys + + +class Env(object): + def __init__(self, src=None): + self._dict = {} + self.update(src or os.environ) + + def update(self, src): + for k, v in src.items(): + self.set(k, v) + + def copy(self): + return Env(self._dict) + + def _key(self, key): + # The `str` cast may not appear to be needed, but without it we run + # into issues when passing the environment to subprocess. The main + # issue is that in python2 `os.environ` (which is the initial source + # of data for the environment) uses byte based strings, but this + # project uses `unicode_literals`. `subprocess` will raise an error + # if the environment that it is passed has a mixture of byte and + # unicode strings. + # It is simplest to force everthing to be `str` for the sake of + # consistency. + key = str(key) + if sys.platform.startswith("win"): + # Windows env var names are case insensitive but case preserving. + # An implementation of PAR files on windows gets confused if + # the env block contains keys with conflicting case, so make a + # pass over the contents to remove any. + # While this O(n) scan is technically expensive and gross, it + # is practically not a problem because the volume of calls is + # relatively low and the cost of manipulating the env is dwarfed + # by the cost of spawning a process on windows. In addition, + # since the processes that we run are expensive anyway, this + # overhead is not the worst thing to worry about. + for k in list(self._dict.keys()): + if str(k).lower() == key.lower(): + return k + elif key in self._dict: + return key + return None + + def get(self, key, defval=None): + key = self._key(key) + if key is None: + return defval + return self._dict[key] + + def __getitem__(self, key): + val = self.get(key) + if key is None: + raise KeyError(key) + return val + + def unset(self, key): + if key is None: + raise KeyError("attempting to unset env[None]") + + key = self._key(key) + if key: + del self._dict[key] + + def __delitem__(self, key): + self.unset(key) + + def __repr__(self): + return repr(self._dict) + + def set(self, key, value): + if key is None: + raise KeyError("attempting to assign env[None] = %r" % value) + + if value is None: + raise ValueError("attempting to assign env[%s] = None" % key) + + # The `str` conversion is important to avoid triggering errors + # with subprocess if we pass in a unicode value; see commentary + # in the `_key` method. + key = str(key) + value = str(value) + + # The `unset` call is necessary on windows where the keys are + # case insensitive. Since this dict is case sensitive, simply + # assigning the value to the new key is not sufficient to remove + # the old value. The `unset` call knows how to match keys and + # remove any potential duplicates. + self.unset(key) + self._dict[key] = value + + def __setitem__(self, key, value): + self.set(key, value) + + def __iter__(self): + return self._dict.__iter__() + + def __len__(self): + return len(self._dict) + + def keys(self): + return self._dict.keys() + + def values(self): + return self._dict.values() + + def items(self): + return self._dict.items() + + +def add_path_entry(env, name, item, append=True, separator=os.pathsep): + """ Cause `item` to be added to the path style env var named + `name` held in the `env` dict. `append` specifies whether + the item is added to the end (the default) or should be + prepended if `name` already exists. """ + val = env.get(name, "") + if len(val) > 0: + val = val.split(separator) + else: + val = [] + if append: + val.append(item) + else: + val.insert(0, item) + env.set(name, separator.join(val)) + + +def add_flag(env, name, flag, append=True): + """ Cause `flag` to be added to the CXXFLAGS-style env var named + `name` held in the `env` dict. `append` specifies whether the + flag is added to the end (the default) or should be prepended if + `name` already exists. """ + val = shlex.split(env.get(name, "")) + if append: + val.append(flag) + else: + val.insert(0, flag) + env.set(name, " ".join(val)) + + +def path_search(env, exename, defval=None): + """ Search for exename in the PATH specified in env. + exename is eg: `ninja` and this function knows to append a .exe + to the end on windows. + Returns the path to the exe if found, or None if either no + PATH is set in env or no executable is found. """ + + path = env.get("PATH", None) + if path is None: + return defval + + is_win = sys.platform.startswith("win") + if is_win: + exename = "%s.exe" % exename + + for bindir in path.split(os.pathsep): + full_name = os.path.join(bindir, exename) + if os.path.exists(full_name) and os.path.isfile(full_name): + if not is_win and not os.access(full_name, os.X_OK): + continue + return full_name + + return None