Skip to content

Commit

Permalink
programs: Allow excluding certain paths when searching in PATH
Browse files Browse the repository at this point in the history
  • Loading branch information
nirbheek committed Oct 4, 2024
1 parent 3c2d04d commit 0f914b7
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 42 deletions.
10 changes: 6 additions & 4 deletions mesonbuild/dependencies/configtool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -52,7 +52,7 @@ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.
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:
Expand Down Expand Up @@ -84,15 +84,17 @@ def _check_and_get_version(self, tool: T.List[str], returncode: int) -> T.Tuple[
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()
Expand Down
2 changes: 1 addition & 1 deletion mesonbuild/dependencies/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> No
['--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]
Expand Down
18 changes: 9 additions & 9 deletions mesonbuild/interpreter/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,7 @@ def run_command_impl(self,
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
Expand Down Expand Up @@ -1586,7 +1586,7 @@ def program_from_file_for(self, for_machine: MachineChoice, prognames: T.List[me
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')
Expand All @@ -1601,15 +1601,15 @@ def program_from_system(self, args: T.List[mesonlib.FileOrString], search_dirs:
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
Expand Down Expand Up @@ -1681,7 +1681,7 @@ def find_program_impl(self, args: T.List[mesonlib.FileOrString],
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],
Expand Down
62 changes: 36 additions & 26 deletions mesonbuild/programs.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,20 @@

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
Expand All @@ -51,13 +57,10 @@ def __init__(self, name: str, command: T.Optional[T.List[str]] = None,
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
Expand Down Expand Up @@ -242,7 +245,7 @@ def _search_dir(self, name: str, search_dir: T.Optional[str]) -> T.Optional[list
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,
Expand Down Expand Up @@ -278,31 +281,37 @@ def _search_windows_special_cases(self, name: str, command: str) -> T.List[T.Opt
# 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]
Expand Down Expand Up @@ -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)
Expand All @@ -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')
4 changes: 2 additions & 2 deletions unittests/failuretests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down

0 comments on commit 0f914b7

Please sign in to comment.