From f33120eacafa3f840bfe96eb744ae18bb7587712 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 10 Jan 2025 15:30:35 +0100 Subject: [PATCH] find_program: add a kwarg to skip searching the source dir Unless search directories or overrides are provided, `find_program()` will by first search for an executable name in the source directory before trying to look it up via the `PATH` environment variable. This can create issues in some projects where the executable exists in the source directory, but when the actual program should be looked up via PATH. While the obvious answer is to move out the given executable from the source directory it's not always feasible. Git for example supports being built with both Makefiles and Meson. In the former case, the resulting `git` executable will be put into the root level source directory. So if one then sets up Meson, we would find that binary instead of the system-provided binary. Add a new "skip_source_dir" kwarg to `find_program()` that allows the user to skip looking up programs via the source directory. Signed-off-by: Patrick Steinhardt --- .../snippets/find_program_skip_source_dir.md | 6 +++++ docs/yaml/functions/find_program.yaml | 9 ++++++++ mesonbuild/interpreter/interpreter.py | 22 +++++++++++++------ .../unit/125 skip_source_dir/executable.py | 3 +++ .../unit/125 skip_source_dir/meson.build | 9 ++++++++ .../125 skip_source_dir/path/executable.py | 3 +++ unittests/allplatformstests.py | 9 ++++++++ 7 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 docs/markdown/snippets/find_program_skip_source_dir.md create mode 100755 test cases/unit/125 skip_source_dir/executable.py create mode 100644 test cases/unit/125 skip_source_dir/meson.build create mode 100755 test cases/unit/125 skip_source_dir/path/executable.py diff --git a/docs/markdown/snippets/find_program_skip_source_dir.md b/docs/markdown/snippets/find_program_skip_source_dir.md new file mode 100644 index 000000000000..0dbbc565b441 --- /dev/null +++ b/docs/markdown/snippets/find_program_skip_source_dir.md @@ -0,0 +1,6 @@ +## `find_program()` can optionally skip searching the source directory + +When given an executable name without any overrides, the `find_program()` +function searches the source directory for the executable before scanning +through `PATH`. This can now be skipped by passing `skip_source_dir: true` to +`find_program()` so that only `PATH` will be searched. diff --git a/docs/yaml/functions/find_program.yaml b/docs/yaml/functions/find_program.yaml index 1899941ab0e8..ed25a4962b61 100644 --- a/docs/yaml/functions/find_program.yaml +++ b/docs/yaml/functions/find_program.yaml @@ -121,6 +121,15 @@ kwargs: since: 0.53.0 description: extra list of absolute paths where to look for program names. + skip_source_dir: + type: bool + since: 1.7.0 + default: false + description: | + Unless `dirs` are provided, Meson searches the source directory relative + to the current subdir before searching through `PATH`. If `true`, this + step will skip searching the source directory and query `PATH` directly. + default_options: type: list[str] | dict[str | bool | int | list[str]] since: 1.3.0 diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 02a59e3986d5..b7a773efd7f7 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1579,11 +1579,15 @@ def program_from_file_for(self, for_machine: MachineChoice, prognames: T.List[me return None 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]: + skip_source_dir: bool, 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') # might give different results when run from different source dirs. - source_dir = os.path.join(self.environment.get_source_dir(), self.subdir) + if skip_source_dir: + source_dir = [] + else: + source_dir = [os.path.join(self.environment.get_source_dir(), self.subdir)] for exename in args: if isinstance(exename, mesonlib.File): if exename.is_built: @@ -1596,9 +1600,9 @@ def program_from_system(self, args: T.List[mesonlib.FileOrString], search_dirs: search_dirs = [search_dir] elif isinstance(exename, str): if search_dirs: - search_dirs = [source_dir] + search_dirs + search_dirs = source_dir + search_dirs else: - search_dirs = [source_dir] + search_dirs = source_dir else: raise InvalidArguments(f'find_program only accepts strings and files, not {exename!r}') extprog = ExternalProgram(exename, search_dirs=search_dirs, silent=True) @@ -1647,13 +1651,14 @@ def find_program_impl(self, args: T.List[mesonlib.FileOrString], required: bool = True, silent: bool = True, wanted: T.Union[str, T.List[str]] = '', search_dirs: T.Optional[T.List[str]] = None, + skip_source_dir: bool = False, version_arg: T.Optional[str] = '', version_func: T.Optional[ProgramVersionFunc] = None ) -> T.Union['ExternalProgram', 'build.Executable', 'OverrideProgram']: args = mesonlib.listify(args) extra_info: T.List[mlog.TV_Loggable] = [] - progobj = self.program_lookup(args, for_machine, default_options, required, search_dirs, wanted, version_arg, version_func, extra_info) + progobj = self.program_lookup(args, for_machine, default_options, required, search_dirs, skip_source_dir, wanted, version_arg, version_func, extra_info) if progobj is None or not self.check_program_version(progobj, wanted, version_func, extra_info): progobj = self.notfound_program(args) @@ -1677,6 +1682,7 @@ def program_lookup(self, args: T.List[mesonlib.FileOrString], for_machine: Machi default_options: T.Optional[T.Dict[OptionKey, T.Union[str, int, bool, T.List[str]]]], required: bool, search_dirs: T.Optional[T.List[str]], + skip_source_dir: bool, wanted: T.Union[str, T.List[str]], version_arg: T.Optional[str], version_func: T.Optional[ProgramVersionFunc], @@ -1699,7 +1705,7 @@ def program_lookup(self, args: T.List[mesonlib.FileOrString], for_machine: Machi progobj = self.program_from_file_for(for_machine, args) if progobj is None: - progobj = self.program_from_system(args, search_dirs, extra_info) + progobj = self.program_from_system(args, search_dirs, skip_source_dir, extra_info) if progobj is None and args[0].endswith('python3'): prog = ExternalProgram('python3', mesonlib.python_command, silent=True) progobj = prog if prog.found() else None @@ -1764,6 +1770,7 @@ def find_program_fallback(self, fallback: str, args: T.List[mesonlib.FileOrStrin NATIVE_KW, REQUIRED_KW, KwargInfo('dirs', ContainerTypeInfo(list, str), default=[], listify=True, since='0.53.0'), + KwargInfo('skip_source_dir', bool, default=False, since='1.7.0'), KwargInfo('version', ContainerTypeInfo(list, str), default=[], listify=True, since='0.52.0'), KwargInfo('version_argument', str, default='', since='1.5.0'), DEFAULT_OPTIONS.evolve(since='1.3.0') @@ -1780,9 +1787,10 @@ def func_find_program(self, node: mparser.BaseNode, args: T.Tuple[T.List[mesonli search_dirs = extract_search_dirs(kwargs) default_options = kwargs['default_options'] + skip_source_dir = kwargs['skip_source_dir'] return self.find_program_impl(args[0], kwargs['native'], default_options=default_options, required=required, silent=False, wanted=kwargs['version'], version_arg=kwargs['version_argument'], - search_dirs=search_dirs) + search_dirs=search_dirs, skip_source_dir=skip_source_dir) # When adding kwargs, please check if they make sense in dependencies.get_dep_identifier() @FeatureNewKwargs('dependency', '0.57.0', ['cmake_package_version']) diff --git a/test cases/unit/125 skip_source_dir/executable.py b/test cases/unit/125 skip_source_dir/executable.py new file mode 100755 index 000000000000..123416e8006c --- /dev/null +++ b/test cases/unit/125 skip_source_dir/executable.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 + +print('from source directory') diff --git a/test cases/unit/125 skip_source_dir/meson.build b/test cases/unit/125 skip_source_dir/meson.build new file mode 100644 index 000000000000..60b4b75d2e9e --- /dev/null +++ b/test cases/unit/125 skip_source_dir/meson.build @@ -0,0 +1,9 @@ +project('skip_source_dir', + meson_version: '>=1.7.0', +) + +summary({ + 'no args': run_command(find_program('executable.py'), capture: true, check: true).stdout(), + 'skip_source_dir: true': run_command(find_program('executable.py', skip_source_dir: false), capture: true, check: true).stdout(), + 'skip_source_dir: false': run_command(find_program('executable.py', skip_source_dir: true), capture: true, check: true).stdout(), +}, section: 'output') diff --git a/test cases/unit/125 skip_source_dir/path/executable.py b/test cases/unit/125 skip_source_dir/path/executable.py new file mode 100755 index 000000000000..53b4f6e88294 --- /dev/null +++ b/test cases/unit/125 skip_source_dir/path/executable.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 + +print('from path') diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py index 96576b0ee888..920b00e42cab 100644 --- a/unittests/allplatformstests.py +++ b/unittests/allplatformstests.py @@ -5076,6 +5076,15 @@ def test_c_cpp_stds(self): self.setconf('-Dcpp_std=c++11,gnu++11,vc++11') self.assertEqual(self.getconf('cpp_std'), 'c++11') + def test_skip_source_dir(self): + testdir = os.path.join(self.unit_test_dir, '125 skip_source_dir') + env = os.environ.copy() + env['PATH'] = os.path.join(testdir, 'path') + os.pathsep + env['PATH'] + output = self.init(testdir, override_envvars=env) + self.assertRegex(output, r'no args *: from source directory') + self.assertRegex(output, r'skip_source_dir: true *: from source directory') + self.assertRegex(output, r'skip_source_dir: false *: from path') + def test_rsp_support(self): env = get_fake_env() cc = detect_c_compiler(env, MachineChoice.HOST)