Skip to content

Commit

Permalink
find_program: add a kwarg to skip searching the source dir
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
pks-t committed Jan 11, 2025
1 parent a86476c commit f33120e
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 7 deletions.
6 changes: 6 additions & 0 deletions docs/markdown/snippets/find_program_skip_source_dir.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 9 additions & 0 deletions docs/yaml/functions/find_program.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 15 additions & 7 deletions mesonbuild/interpreter/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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],
Expand All @@ -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
Expand Down Expand Up @@ -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')
Expand All @@ -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'])
Expand Down
3 changes: 3 additions & 0 deletions test cases/unit/125 skip_source_dir/executable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env python3

print('from source directory')
9 changes: 9 additions & 0 deletions test cases/unit/125 skip_source_dir/meson.build
Original file line number Diff line number Diff line change
@@ -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')
3 changes: 3 additions & 0 deletions test cases/unit/125 skip_source_dir/path/executable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env python3

print('from path')
9 changes: 9 additions & 0 deletions unittests/allplatformstests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit f33120e

Please sign in to comment.