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 <ps@pks.im>
pull/14119/head
Patrick Steinhardt 1 month ago
parent a86476c57c
commit f33120eaca
  1. 6
      docs/markdown/snippets/find_program_skip_source_dir.md
  2. 9
      docs/yaml/functions/find_program.yaml
  3. 22
      mesonbuild/interpreter/interpreter.py
  4. 3
      test cases/unit/125 skip_source_dir/executable.py
  5. 9
      test cases/unit/125 skip_source_dir/meson.build
  6. 3
      test cases/unit/125 skip_source_dir/path/executable.py
  7. 9
      unittests/allplatformstests.py

@ -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.

@ -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

@ -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'])

@ -0,0 +1,3 @@
#!/usr/bin/env python3
print('from source directory')

@ -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')

@ -0,0 +1,3 @@
#!/usr/bin/env python3
print('from path')

@ -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)

Loading…
Cancel
Save