Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support user-specified config-tool programs #13660

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/markdown/Dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,28 @@ objfw_dep = dependency('objfw', version: '>= 1.0')
binary when cross compiling if the config tool did not have an entry
in the cross file.

## User-specified config tool

If a dependency has a config tool and it is not natively supported by meson,
you can specify it directly as the dependency method:

```meson
config_tool = find_program('my-config-tool')
dep = dependency('some-dep', method: config_tool)
```

The config tool program **must** support `--cflags`, `--libs`, and `--version`.

If it requires additional args, you can specify them. The special value `@NAME@`
will be replaced with the dependency name if found:

```meson
config_tool = find_program('my-config-tool')
dep = dependency('some-dep', method: [config_tool, '--package', '@NAME@'])
```

*(added 1.6.0)*

# Dependencies with custom lookup functionality

Some dependencies have specific detection logic.
Expand Down
12 changes: 12 additions & 0 deletions docs/markdown/snippets/user_config_tool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## New user-specified config tool support

The `method` keyword for `dependency()` can now take an external program as
an argument:

```meson
config_tool = find_program('my-config-tool')
dep = dependency('some-dep', method: config_tool)

# or pass required arguments to it
dep = dependency('some-dep', method: [config_tool, '--package', '@NAME'])
```
8 changes: 7 additions & 1 deletion docs/yaml/functions/dependency.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ kwargs:
if it's available for multiple languages.

method:
type: str
type: str | external_program | list[external_program | str]
since: 0.40.0
default: "'auto'"
description: |
Expand All @@ -141,6 +141,12 @@ kwargs:
Dependencies.md#dependencies-with-custom-lookup-functionality)
for this (though `auto` will work on all of them)

*(since 1.6.0)* a dependency can be queried from a user-specified
config tool. That program must support `--cflags`, `--libs`, and
`--version` arguments. Additional arguments to the program can be
specified as a list (but the first argument must be the external_program).
The special value `@NAME@` will be replaced with the dependency name.

native:
type: bool
default: false
Expand Down
65 changes: 37 additions & 28 deletions mesonbuild/dependencies/configtool.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from .base import ExternalDependency, DependencyException, DependencyTypeName
from ..mesonlib import listify, Popen_safe, Popen_safe_logged, split_args, version_compare, version_compare_many
from ..programs import find_external_program
from ..programs import find_external_program, ExternalProgram
from .. import mlog
import re
import typing as T
Expand Down Expand Up @@ -52,7 +52,7 @@
req_version = mesonlib.stringlistify(req_version_raw)
else:
req_version = []
tool, version = self.find_config(req_version, kwargs.get('returncode_value', 0))

Check warning

Code scanning / CodeQL

`__init__` method calls overridden method Warning

Call to self.
find_config
in __init__ method, which is overridden by
method UserConfigToolDependency.find_config
.
Call to self.
find_config
in __init__ method, which is overridden by
method GnuStepDependency.find_config
.
self.config = tool
self.is_found = self.report_config(version, req_version)
if not self.is_found:
Expand All @@ -78,35 +78,11 @@
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):
if not potential_bin.found():
tool, out = self._check_config(potential_bin, [], versions, returncode)
if tool is None and out is None:
continue
tool = potential_bin.get_command()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of churn here, but it's just refactoring to the _check_config function. Open to ideas to make this churn less.

try:
p, out = Popen_safe(tool + [self.version_arg])[:2]
except (FileNotFoundError, PermissionError):
continue
if p.returncode != returncode:
if self.skip_version:
# maybe the executable is valid even if it doesn't support --version
p = Popen_safe(tool + [self.skip_version])[0]
if p.returncode != returncode:
continue
else:
continue

out = self._sanitize_version(out.strip())
# Some tools, like pcap-config don't supply a version, but also
# don't fail with --version, in that case just assume that there is
# only one version and return it.
if not out:
elif out is None:
return (tool, None)
if versions:
is_found = version_compare_many(out, versions)[0]
# This allows returning a found version without a config tool,
# which is useful to inform the user that you found version x,
# but y was required.
if not is_found:
tool = None
if best_match[1]:
if version_compare(out, '> {}'.format(best_match[1])):
best_match = (tool, out)
Expand All @@ -115,6 +91,39 @@

return best_match

def _check_config(self, potential_bin: ExternalProgram, args: T.List[str], versions: T.List[str], returncode: int = 0) \
-> T.Tuple[T.Optional[T.List[str]], T.Optional[str]]:
if not potential_bin.found():
return (None, None)
tool = potential_bin.get_command() + args
try:
p, out = Popen_safe(tool + [self.version_arg])[:2]
except (FileNotFoundError, PermissionError):
return (None, None)
if p.returncode != returncode:
if self.skip_version:
# maybe the executable is valid even if it doesn't support --version
p = Popen_safe(tool + [self.skip_version])[0]
if p.returncode != returncode:
return (None, None)
else:
return (None, None)

out = self._sanitize_version(out.strip())
# Some tools, like pcap-config don't supply a version, but also
# don't fail with --version, in that case just assume that there is
# only one version and return it.
if not out:
return (tool, None)
if versions:
is_found = version_compare_many(out, versions)[0]
# This allows returning a found version without a config tool,
# which is useful to inform the user that you found version x,
# but y was required.
if not is_found:
tool = None
return tool, out
Comment on lines +112 to +125
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is going to be done, I think it'd be great to take the opportunity to stop calling the version variable out, because that is a terrible name.


def report_config(self, version: T.Optional[str], req_version: T.List[str]) -> bool:
"""Helper method to print messages about the tool."""

Expand Down
43 changes: 36 additions & 7 deletions mesonbuild/dependencies/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from ..mesonlib import listify, MachineChoice, PerMachine
from .. import mlog
from ..programs import ExternalProgram

if T.TYPE_CHECKING:
from ..environment import Environment
Expand Down Expand Up @@ -56,8 +57,11 @@ def get_dep_identifier(name: str, kwargs: T.Dict[str, T.Any]) -> 'TV_DepID':
if key in {'version', 'native', 'required', 'fallback', 'allow_fallback', 'default_options',
'not_found_message', 'include_type'}:
continue
# All keyword arguments are strings, ints, or lists (or lists of lists)
if isinstance(value, list):
# All other keyword arguments are strings, ints, or lists (or lists of lists)
if isinstance(value, ExternalProgram):
value = value.get_path()
elif isinstance(value, list):
value = [i.get_path() if isinstance(i, ExternalProgram) else i for i in value]
for i in value:
assert isinstance(i, str)
value = tuple(frozenset(listify(value)))
Expand Down Expand Up @@ -85,8 +89,8 @@ def find_external_dependency(name: str, env: 'Environment', kwargs: T.Dict[str,
required = kwargs.get('required', True)
if not isinstance(required, bool):
raise DependencyException('Keyword "required" must be a boolean.')
if not isinstance(kwargs.get('method', ''), str):
raise DependencyException('Keyword "method" must be a string.')
if not isinstance(kwargs.get('method', ''), (str, ExternalProgram, list)):
raise DependencyException('Keyword "method" must be a string or external_program or list.')
lname = name.lower()
if lname not in _packages_accept_language and 'language' in kwargs:
raise DependencyException(f'{name} dependency does not accept "language" keyword argument')
Expand Down Expand Up @@ -171,8 +175,29 @@ def find_external_dependency(name: str, env: 'Environment', kwargs: T.Dict[str,
def _build_external_dependency_list(name: str, env: 'Environment', for_machine: MachineChoice,
kwargs: T.Dict[str, T.Any]) -> T.List['DependencyGenerator']:
# First check if the method is valid
if 'method' in kwargs and kwargs['method'] not in [e.value for e in DependencyMethods]:
raise DependencyException('method {!r} is invalid'.format(kwargs['method']))
method: T.Union[str, ExternalProgram, T.List[T.Union[ExternalProgram, str]]] = kwargs.get('method', 'auto')
user_method: T.Optional[T.Tuple[ExternalProgram, T.List[str]]] = None
if isinstance(method, str):
if method not in [e.value for e in DependencyMethods]:
raise DependencyException('method {!r} is invalid'.format(kwargs['method']))

elif isinstance(method, list):
if len(method) == 0:
raise DependencyException('method must not be empty list')
elif not isinstance(method[0], ExternalProgram):
raise DependencyException('method first list element must be external program')
else:
args: T.List[str] = []
for elem in method[1:]:
if not isinstance(elem, str):
raise DependencyException('method list[1:] elements must be str')
args.append(elem)
user_method = (method[0], args)

elif isinstance(method, ExternalProgram):
user_method = (method, [])
else:
raise DependencyException('method must be str or list or external program')

# Is there a specific dependency detector for this dependency?
lname = name.lower()
Expand All @@ -192,9 +217,13 @@ def _build_external_dependency_list(name: str, env: 'Environment', for_machine:

candidates: T.List['DependencyGenerator'] = []

if kwargs.get('method', 'auto') == 'auto':
if method == 'auto':
# Just use the standard detection methods.
methods = ['pkg-config', 'extraframework', 'cmake']
elif user_method is not None:
from .misc import UserConfigToolDependency
candidates.append(functools.partial(UserConfigToolDependency, name, user_method, env, kwargs))
methods = []
else:
# If it's explicitly requested, use that detection method (only).
methods = [kwargs['method']]
Expand Down
23 changes: 23 additions & 0 deletions mesonbuild/dependencies/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .factory import DependencyFactory, factory_methods
from .pkgconfig import PkgConfigDependency
from ..options import OptionKey
from ..programs import ExternalProgram

if T.TYPE_CHECKING:
from ..environment import Environment
Expand Down Expand Up @@ -495,6 +496,28 @@ def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
self.link_args = self.get_config_value(['--ldflags', '--libs'] + extra_flags, 'link_args')


class UserConfigToolDependency(ConfigToolDependency):
def __init__(self, name: str, user_method: T.Tuple[ExternalProgram, T.List[str]], environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None):
program, args = user_method
args = [name if arg == "@NAME@" else arg for arg in args]
self.user_method = (program, args)

self.tools = [program.get_path()]

super().__init__(name, environment, kwargs, language)

if not self.is_found:
return

self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
self.link_args = self.get_config_value(['--libs'], 'link_args')

def find_config(self, versions: T.List[str], returncode: int = 0) \
-> T.Tuple[T.Optional[T.List[str]], T.Optional[str]]:
program, args = self.user_method
return self._check_config(program, args, versions, returncode)


@factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.SYSTEM})
def curses_factory(env: 'Environment',
for_machine: 'mesonlib.MachineChoice',
Expand Down
8 changes: 6 additions & 2 deletions mesonbuild/interpreter/dependencyfallbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ..wrap import WrapMode
from ..mesonlib import extract_as_list, stringlistify, version_compare_many, listify
from ..options import OptionKey
from ..programs import ExternalProgram
from ..dependencies import Dependency, DependencyException, NotFoundDependency
from ..interpreterbase import (MesonInterpreterObject, FeatureNew,
InterpreterException, InvalidArguments)
Expand Down Expand Up @@ -84,7 +85,7 @@ def _do_dependency(self, kwargs: TYPE_nkwargs, func_args: TYPE_nvar, func_kwargs
# We use kwargs from the dependency() function, for things like version,
# module, etc.
name = func_args[0]
self._handle_featurenew_dependencies(name)
self._handle_featurenew_dependencies(name, kwargs.get('method', None))
dep = dependencies.find_external_dependency(name, self.environment, kwargs)
if dep.found():
for_machine = self.interpreter.machine_from_native_kwarg(kwargs)
Expand Down Expand Up @@ -266,7 +267,7 @@ def _verify_fallback_consistency(self, cached_dep: Dependency) -> None:
if var_dep and cached_dep.found() and var_dep != cached_dep:
mlog.warning(f'Inconsistency: Subproject has overridden the dependency with another variable than {varname!r}')

def _handle_featurenew_dependencies(self, name: str) -> None:
def _handle_featurenew_dependencies(self, name: str, method: T.Any) -> None:
'Do a feature check on dependencies used by this subproject'
if name == 'mpi':
FeatureNew.single_use('MPI Dependency', '0.42.0', self.subproject)
Expand All @@ -279,6 +280,9 @@ def _handle_featurenew_dependencies(self, name: str) -> None:
elif name == 'openmp':
FeatureNew.single_use('OpenMP Dependency', '0.46.0', self.subproject)

if isinstance(method, (ExternalProgram, list)):
FeatureNew.single_use('Keyword "method" as an ExternalProgram', '1.6.0', self.subproject)

def _notfound_dependency(self) -> NotFoundDependency:
return NotFoundDependency(self.names[0] if self.names else '', self.environment)

Expand Down
36 changes: 36 additions & 0 deletions test cases/common/278 user config-tool/config-tool-pkg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
#
# User-specified config-tool that has custom positional arguments. must implement --cflags,
# --libs, and --version
#
# Note: meson will not reconfigure if this program or its output changes
#

import pathlib
import platform
import sys


def make_include_arg(p: pathlib.Path) -> str:
if platform.system().lower() == "windows":
return f'"-I{p.absolute()}"'
else:
return f"'-I{p.absolute()}'"


if __name__ == '__main__':
_, module, flag = sys.argv

# pretend we can only find somemod module
if module != 'somemod':
sys.exit(1)

if flag == '--cflags':
somemod = pathlib.Path(__file__).parent / 'somemod'
print(make_include_arg(somemod))
elif flag == '--libs':
print()
elif flag == '--version':
print('43.0')
else:
sys.exit(1)
31 changes: 31 additions & 0 deletions test cases/common/278 user config-tool/config-tool-plain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
#
# User-specified config-tool, must implement --cflags, --libs, and --version
#
# Note: meson will not reconfigure if this program or its output changes
#

import pathlib
import platform
import sys


def make_include_arg(p: pathlib.Path) -> str:
if platform.system().lower() == "windows":
return f'"-I{p.absolute()}"'
else:
return f"'-I{p.absolute()}'"


if __name__ == '__main__':
flag = sys.argv[1]

if flag == '--cflags':
somedep = pathlib.Path(__file__).parent / 'somedep'
print(make_include_arg(somedep))
elif flag == '--libs':
print()
elif flag == '--version':
print('42.0')
else:
sys.exit(1)
7 changes: 7 additions & 0 deletions test cases/common/278 user config-tool/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

#include "somedep.h"
#include "somemod.h"

int main(void) {
return 0;
}
Loading
Loading