1
0
mirror of https://github.com/facebook/proxygen.git synced 2025-08-08 18:02:05 +03:00
Files
proxygen/build/fbcode_builder/getdeps/envfuncs.py
Adam Simpkins 1c878fcda0 cache results of path_search()
Summary:
getdeps currently ends up calling `path_search()` repeatedly searching for
various C/C++ compilers in $PATH.  It ends up doing this multiple times for
each dependency as it computes the project hashes.  This updates the
`path_search()` function to cache its results so that we don't keep performing
the same searches on the file system over and over again.

Reviewed By: chadaustin

Differential Revision: D16354625

fbshipit-source-id: 116293bd2f636632517d26436b2332e6c10624f1
2019-07-19 15:29:06 -07:00

192 lines
6.2 KiB
Python

#!/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))
_path_search_cache = {}
_not_found = object()
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
# The project hash computation code searches for C++ compilers (g++, clang, etc)
# repeatedly. Cache the result so we don't end up searching for these over and over
# again.
cache_key = (path, exename)
result = _path_search_cache.get(cache_key, _not_found)
if result is _not_found:
result = _perform_path_search(path, exename)
_path_search_cache[cache_key] = result
return result
def _perform_path_search(path, exename):
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