diff --git a/mesonbuild/dependencies/configtool.py b/mesonbuild/dependencies/configtool.py index ef106a8b0..476f7ad42 100644 --- a/mesonbuild/dependencies/configtool.py +++ b/mesonbuild/dependencies/configtool.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() diff --git a/mesonbuild/dependencies/ui.py b/mesonbuild/dependencies/ui.py index cc17377a6..d88af7945 100644 --- a/mesonbuild/dependencies/ui.py +++ b/mesonbuild/dependencies/ui.py @@ -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] diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index eceb40a6b..aa839da36 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -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], diff --git a/mesonbuild/programs.py b/mesonbuild/programs.py index bbe8ea421..9ad38e126 100644 --- a/mesonbuild/programs.py +++ b/mesonbuild/programs.py @@ -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') diff --git a/unittests/failuretests.py b/unittests/failuretests.py index baa592047..8a802120b 100644 --- a/unittests/failuretests.py +++ b/unittests/failuretests.py @@ -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':