programs: Allow excluding certain paths when searching in PATH

pull/13748/head
Nirbheek Chauhan 2 months ago committed by Nirbheek Chauhan
parent 3c2d04d702
commit 0f914b75fe
  1. 10
      mesonbuild/dependencies/configtool.py
  2. 2
      mesonbuild/dependencies/ui.py
  3. 18
      mesonbuild/interpreter/interpreter.py
  4. 62
      mesonbuild/programs.py
  5. 4
      unittests/failuretests.py

@ -37,7 +37,7 @@ class ConfigToolDependency(ExternalDependency):
allow_default_for_cross = False
__strip_version = re.compile(r'^[0-9][0-9.]+')
def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None):
def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None, exclude_paths: T.Optional[T.List[str]] = None):
super().__init__(DependencyTypeName('config-tool'), environment, kwargs, language=language)
self.name = name
# You may want to overwrite the class version in some cases
@ -52,7 +52,7 @@ class ConfigToolDependency(ExternalDependency):
req_version = mesonlib.stringlistify(req_version_raw)
else:
req_version = []
tool, version = self.find_config(req_version, kwargs.get('returncode_value', 0))
tool, version = self.find_config(req_version, kwargs.get('returncode_value', 0), exclude_paths=exclude_paths)
self.config = tool
self.is_found = self.report_config(version, req_version)
if not self.is_found:
@ -84,15 +84,17 @@ class ConfigToolDependency(ExternalDependency):
version = self._sanitize_version(out.strip())
return valid, version
def find_config(self, versions: T.List[str], returncode: int = 0) \
def find_config(self, versions: T.List[str], returncode: int = 0, exclude_paths: T.Optional[T.List[str]] = None) \
-> T.Tuple[T.Optional[T.List[str]], T.Optional[str]]:
"""Helper method that searches for config tool binaries in PATH and
returns the one that best matches the given version requirements.
"""
exclude_paths = [] if exclude_paths is None else exclude_paths
best_match: T.Tuple[T.Optional[T.List[str]], T.Optional[str]] = (None, None)
for potential_bin in find_external_program(
self.env, self.for_machine, self.tool_name,
self.tool_name, self.tools, allow_default_for_cross=self.allow_default_for_cross):
self.tool_name, self.tools, exclude_paths=exclude_paths,
allow_default_for_cross=self.allow_default_for_cross):
if not potential_bin.found():
continue
tool = potential_bin.get_command()

@ -68,7 +68,7 @@ class GnuStepDependency(ConfigToolDependency):
['--gui-libs' if 'gui' in self.modules else '--base-libs'],
'link_args'))
def find_config(self, versions: T.Optional[T.List[str]] = None, returncode: int = 0) -> T.Tuple[T.Optional[T.List[str]], T.Optional[str]]:
def find_config(self, versions: T.Optional[T.List[str]] = None, returncode: int = 0, exclude_paths: T.Optional[T.List[str]] = None) -> T.Tuple[T.Optional[T.List[str]], T.Optional[str]]:
tool = [self.tools[0]]
try:
p, out = Popen_safe(tool + ['--help'])[:2]

@ -816,7 +816,7 @@ class Interpreter(InterpreterBase, HoldableObject):
cmd = cmd.absolute_path(srcdir, builddir)
# Prefer scripts in the current source directory
search_dir = os.path.join(srcdir, self.subdir)
prog = ExternalProgram(cmd, silent=True, search_dir=search_dir)
prog = ExternalProgram(cmd, silent=True, search_dirs=[search_dir])
if not prog.found():
raise InterpreterException(f'Program or command {cmd!r} not found or not executable')
cmd = prog
@ -1586,7 +1586,7 @@ class Interpreter(InterpreterBase, HoldableObject):
return prog
return None
def program_from_system(self, args: T.List[mesonlib.FileOrString], search_dirs: T.List[str],
def program_from_system(self, args: T.List[mesonlib.FileOrString], search_dirs: T.Optional[T.List[str]],
extra_info: T.List[mlog.TV_Loggable]) -> T.Optional[ExternalProgram]:
# Search for scripts relative to current subdir.
# Do not cache found programs because find_program('foobar')
@ -1601,15 +1601,15 @@ class Interpreter(InterpreterBase, HoldableObject):
search_dir = os.path.join(self.environment.get_source_dir(),
exename.subdir)
exename = exename.fname
extra_search_dirs = []
search_dirs = [search_dir]
elif isinstance(exename, str):
search_dir = source_dir
extra_search_dirs = search_dirs
if search_dirs:
search_dirs = [source_dir] + search_dirs
else:
search_dirs = [source_dir]
else:
raise InvalidArguments(f'find_program only accepts strings and files, not {exename!r}')
extprog = ExternalProgram(exename, search_dir=search_dir,
extra_search_dirs=extra_search_dirs,
silent=True)
extprog = ExternalProgram(exename, search_dirs=search_dirs, silent=True)
if extprog.found():
extra_info.append(f"({' '.join(extprog.get_command())})")
return extprog
@ -1681,7 +1681,7 @@ class Interpreter(InterpreterBase, HoldableObject):
def program_lookup(self, args: T.List[mesonlib.FileOrString], for_machine: MachineChoice,
default_options: T.Optional[T.Dict[OptionKey, T.Union[str, int, bool, T.List[str]]]],
required: bool,
search_dirs: T.List[str],
search_dirs: T.Optional[T.List[str]],
wanted: T.Union[str, T.List[str]],
version_arg: T.Optional[str],
version_func: T.Optional[ProgramVersionFunc],

@ -25,14 +25,20 @@ if T.TYPE_CHECKING:
class ExternalProgram(mesonlib.HoldableObject):
"""A program that is found on the system."""
"""A program that is found on the system.
:param name: The name of the program
:param command: Optionally, an argument list constituting the command. Used when
you already know the command and do not want to search.
:param silent: Whether to print messages when initializing
:param search_dirs: A list of directories to search in first, followed by PATH
:param exclude_paths: A list of directories to exclude when searching in PATH"""
windows_exts = ('exe', 'msc', 'com', 'bat', 'cmd')
for_machine = MachineChoice.BUILD
def __init__(self, name: str, command: T.Optional[T.List[str]] = None,
silent: bool = False, search_dir: T.Optional[str] = None,
extra_search_dirs: T.Optional[T.List[str]] = None):
silent: bool = False, search_dirs: T.Optional[T.List[T.Optional[str]]] = None,
exclude_paths: T.Optional[T.List[str]] = None):
self.name = name
self.path: T.Optional[str] = None
self.cached_version: T.Optional[str] = None
@ -51,13 +57,10 @@ class ExternalProgram(mesonlib.HoldableObject):
else:
self.command = [cmd] + args
else:
all_search_dirs = [search_dir]
if extra_search_dirs:
all_search_dirs += extra_search_dirs
for d in all_search_dirs:
self.command = self._search(name, d)
if self.found():
break
if search_dirs is None:
# For compat with old behaviour
search_dirs = [None]
self.command = self._search(name, search_dirs, exclude_paths)
if self.found():
# Set path to be the last item that is actually a file (in order to
@ -242,7 +245,7 @@ class ExternalProgram(mesonlib.HoldableObject):
return [trial_ext]
return None
def _search_windows_special_cases(self, name: str, command: str) -> T.List[T.Optional[str]]:
def _search_windows_special_cases(self, name: str, command: T.Optional[str], exclude_paths: T.Optional[T.List[str]]) -> T.List[T.Optional[str]]:
'''
Lots of weird Windows quirks:
1. PATH search for @name returns files with extensions from PATHEXT,
@ -278,31 +281,37 @@ class ExternalProgram(mesonlib.HoldableObject):
# On Windows, interpreted scripts must have an extension otherwise they
# cannot be found by a standard PATH search. So we do a custom search
# where we manually search for a script with a shebang in PATH.
search_dirs = self._windows_sanitize_path(os.environ.get('PATH', '')).split(';')
search_dirs = OrderedSet(self._windows_sanitize_path(os.environ.get('PATH', '')).split(';'))
if exclude_paths:
search_dirs.difference_update(exclude_paths)
for search_dir in search_dirs:
commands = self._search_dir(name, search_dir)
if commands:
return commands
return [None]
def _search(self, name: str, search_dir: T.Optional[str]) -> T.List[T.Optional[str]]:
def _search(self, name: str, search_dirs: T.List[T.Optional[str]], exclude_paths: T.Optional[T.List[str]]) -> T.List[T.Optional[str]]:
'''
Search in the specified dir for the specified executable by name
Search in the specified dirs for the specified executable by name
and if not found search in PATH
'''
commands = self._search_dir(name, search_dir)
if commands:
return commands
for search_dir in search_dirs:
commands = self._search_dir(name, search_dir)
if commands:
return commands
# If there is a directory component, do not look in PATH
if os.path.dirname(name) and not os.path.isabs(name):
return [None]
# Do a standard search in PATH
path = os.environ.get('PATH', None)
path = os.environ.get('PATH', os.defpath)
if mesonlib.is_windows() and path:
path = self._windows_sanitize_path(path)
if exclude_paths:
paths = OrderedSet(path.split(os.pathsep)).difference(exclude_paths)
path = os.pathsep.join(paths)
command = shutil.which(name, path=path)
if mesonlib.is_windows():
return self._search_windows_special_cases(name, command)
return self._search_windows_special_cases(name, command, exclude_paths)
# On UNIX-like platforms, shutil.which() is enough to find
# all executables whether in PATH or with an absolute path
return [command]
@ -341,15 +350,16 @@ class OverrideProgram(ExternalProgram):
"""A script overriding a program."""
def __init__(self, name: str, version: str, command: T.Optional[T.List[str]] = None,
silent: bool = False, search_dir: T.Optional[str] = None,
extra_search_dirs: T.Optional[T.List[str]] = None):
silent: bool = False, search_dirs: T.Optional[T.List[T.Optional[str]]] = None,
exclude_paths: T.Optional[T.List[str]] = None):
self.cached_version = version
super().__init__(name, command=command, silent=silent,
search_dir=search_dir, extra_search_dirs=extra_search_dirs)
search_dirs=search_dirs, exclude_paths=exclude_paths)
def find_external_program(env: 'Environment', for_machine: MachineChoice, name: str,
display_name: str, default_names: T.List[str],
allow_default_for_cross: bool = True) -> T.Generator['ExternalProgram', None, None]:
allow_default_for_cross: bool = True,
exclude_paths: T.Optional[T.List[str]] = None) -> T.Generator['ExternalProgram', None, None]:
"""Find an external program, checking the cross file plus any default options."""
potential_names = OrderedSet(default_names)
potential_names.add(name)
@ -367,8 +377,8 @@ def find_external_program(env: 'Environment', for_machine: MachineChoice, name:
# Fallback on hard-coded defaults, if a default binary is allowed for use
# with cross targets, or if this is not a cross target
if allow_default_for_cross or not (for_machine is MachineChoice.HOST and env.is_cross_build(for_machine)):
for potential_path in default_names:
mlog.debug(f'Trying a default {display_name} fallback at', potential_path)
yield ExternalProgram(potential_path, silent=True)
for potential_name in default_names:
mlog.debug(f'Trying a default {display_name} fallback at', potential_name)
yield ExternalProgram(potential_name, silent=True, exclude_paths=exclude_paths)
else:
mlog.debug('Default target is not allowed for cross use')

@ -34,10 +34,10 @@ def no_pkgconfig():
old_which = shutil.which
old_search = ExternalProgram._search
def new_search(self, name, search_dir):
def new_search(self, name, search_dirs, exclude_paths):
if name == 'pkg-config':
return [None]
return old_search(self, name, search_dir)
return old_search(self, name, search_dirs, exclude_paths)
def new_which(cmd, *kwargs):
if cmd == 'pkg-config':

Loading…
Cancel
Save