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 000000000..0dbbc565b --- /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 1899941ab..ed25a4962 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 02a59e398..b7a773efd 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1579,11 +1579,15 @@ class Interpreter(InterpreterBase, HoldableObject): 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 @@ class Interpreter(InterpreterBase, HoldableObject): 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 @@ class Interpreter(InterpreterBase, HoldableObject): 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 @@ class Interpreter(InterpreterBase, HoldableObject): 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 @@ class Interpreter(InterpreterBase, HoldableObject): 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 @@ class Interpreter(InterpreterBase, HoldableObject): 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 @@ class Interpreter(InterpreterBase, HoldableObject): 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 000000000..123416e80 --- /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 000000000..60b4b75d2 --- /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 000000000..53b4f6e88 --- /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 96576b0ee..920b00e42 100644 --- a/unittests/allplatformstests.py +++ b/unittests/allplatformstests.py @@ -5076,6 +5076,15 @@ class AllPlatformTests(BasePlatformTests): 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)