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

Make it possible to override find_program [skip ci] #3218

Merged
merged 9 commits into from
Apr 16, 2018
7 changes: 7 additions & 0 deletions docs/markdown/Reference-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,13 @@ the following methods.
/path/to/meson.py introspect`. The user is responsible for splitting
the string to an array if needed.

- `override_find_program(progname, program)` [*(Added
0.46.0)*](Release-notes-for-0-46-0.html#Can-override-find_program)
specifies that whenever `find_program` is used to find a program
named `progname`, Meson should not not look it up on the system but
instead return `program`, which may either be the result of
`find_program` or `configure_file`.

- `project_version()` returns the version string specified in `project` function call.

- `project_license()` returns the array of licenses specified in `project` function call.
Expand Down
37 changes: 37 additions & 0 deletions docs/markdown/snippets/find-override.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## Can override find_program

It is now possible to override the result of `find_program` to point
to a custom program you want. The overriding is global and applies to
every subproject from there on. Here is how you would use it.

In master project

```meson
subproject('mydep')
```

In the called subproject:

```meson
prog = find_program('my_custom_script')
meson.override_find_program('mycodegen', prog)
Copy link
Member

Choose a reason for hiding this comment

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

This needs an example for configure_file() too.

Copy link
Member Author

Choose a reason for hiding this comment

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

It should probably go to actual documentation, people on average don't read old release notes for stuff. That would require writing a proper page for that, though...

Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should start linking to the release notes for each feature; the (added in v0.xx.y) text is a good candidate for that.

```

In master project (or, in fact, any subproject):

```meson
genprog = find_program('mycodegen')
```

Now `genprog` points to the custom script. If the dependency had come
from the system, then it would point to the system version.

You can also use the return value of `configure_file()` to override
a program in the same way as above:

```meson
prog_script = configure_file(input : 'script.sh.in',
output : 'script.sh',
configuration : cdata)
meson.override_find_program('mycodegen', prog_script)
```
2 changes: 2 additions & 0 deletions mesonbuild/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ def __init__(self, environment):
self.dep_manifest = {}
self.cross_stdlibs = {}
self.test_setups = {}
self.find_overrides = {}
self.searched_programs = set() # The list of all programs that have been searched for.

def add_compiler(self, compiler):
if self.static_linker is None and compiler.needs_static_linker():
Expand Down
1 change: 0 additions & 1 deletion mesonbuild/coredata.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ def __init__(self, options):
self.compilers = OrderedDict()
self.cross_compilers = OrderedDict()
self.deps = OrderedDict()
self.modules = {}
# Only to print a warning if it changes between Meson invocations.
self.pkgconf_envvar = os.environ.get('PKG_CONFIG_PATH', '')

Expand Down
110 changes: 85 additions & 25 deletions mesonbuild/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,7 @@ def __init__(self, build, interpreter):
'add_install_script': self.add_install_script_method,
'add_postconf_script': self.add_postconf_script_method,
'install_dependency_manifest': self.install_dependency_manifest_method,
'override_find_program': self.override_find_program_method,
'project_version': self.project_version_method,
'project_license': self.project_license_method,
'version': self.version_method,
Expand Down Expand Up @@ -1416,6 +1417,26 @@ def install_dependency_manifest_method(self, args, kwargs):
raise InterpreterException('Argument must be a string.')
self.build.dep_manifest_name = args[0]

def override_find_program_method(self, args, kwargs):
if len(args) != 2:
raise InterpreterException('Override needs two arguments')
name = args[0]
exe = args[1]
if not isinstance(name, str):
raise InterpreterException('First argument must be a string')
if hasattr(exe, 'held_object'):
exe = exe.held_object
if isinstance(exe, mesonlib.File):
abspath = exe.absolute_path(self.interpreter.environment.source_dir,
self.interpreter.environment.build_dir)
if not os.path.exists(abspath):
raise InterpreterException('Tried to override %s with a file that does not exist.' % name)
exe = dependencies.ExternalProgram(abspath)
if not isinstance(exe, dependencies.ExternalProgram):
# FIXME, make this work if the exe is an Executable target.
raise InterpreterException('Second argument must be an external program.')
self.interpreter.add_find_program_override(name, exe)

def project_version_method(self, args, kwargs):
return self.build.dep_manifest[self.interpreter.active_projectname]['version']

Expand Down Expand Up @@ -1493,14 +1514,18 @@ def get_cross_property_method(self, args, kwargs):
class Interpreter(InterpreterBase):

def __init__(self, build, backend, subproject='', subdir='', subproject_dir='subprojects',
default_project_options=[]):
modules = None, default_project_options=[]):
super().__init__(build.environment.get_source_dir(), subdir)
self.an_unpicklable_object = mesonlib.an_unpicklable_object
self.build = build
self.environment = build.environment
self.coredata = self.environment.get_coredata()
self.backend = backend
self.subproject = subproject
if modules is None:
self.modules = {}
else:
self.modules = modules
# Subproject directory is usually the name of the subproject, but can
# be different for dependencies provided by wrap files.
self.subproject_directory_name = subdir.split(os.path.sep)[-1]
Expand Down Expand Up @@ -1681,13 +1706,13 @@ def func_import(self, node, args, kwargs):
plainname = modname.split('-', 1)[1]
mlog.warning('Module %s has no backwards or forwards compatibility and might not exist in future releases.' % modname, location=node)
modname = 'unstable_' + plainname
if modname not in self.environment.coredata.modules:
if modname not in self.modules:
try:
module = importlib.import_module('mesonbuild.modules.' + modname)
except ImportError:
raise InvalidArguments('Module "%s" does not exist' % (modname, ))
self.environment.coredata.modules[modname] = module.initialize()
return ModuleHolder(modname, self.environment.coredata.modules[modname], self)
self.modules[modname] = module.initialize(self)
return ModuleHolder(modname, self.modules[modname], self)

@stringArgs
@noKwargs
Expand Down Expand Up @@ -1864,15 +1889,16 @@ def do_subproject(self, dirname, kwargs):
self.global_args_frozen = True
mlog.log()
with mlog.nested():
mlog.log('Executing subproject ', mlog.bold(dirname), '.\n', sep='')
mlog.log('\nExecuting subproject ', mlog.bold(dirname), '.\n', sep='')
subi = Interpreter(self.build, self.backend, dirname, subdir, self.subproject_dir,
mesonlib.stringlistify(kwargs.get('default_options', [])))
self.modules, mesonlib.stringlistify(kwargs.get('default_options', [])))
subi.subprojects = self.subprojects

subi.subproject_stack = self.subproject_stack + [dirname]
current_active = self.active_projectname
subi.run()
mlog.log('\nSubproject', mlog.bold(dirname), 'finished.')

if 'version' in kwargs:
pv = subi.project_version
wanted = kwargs['version']
Expand Down Expand Up @@ -2225,7 +2251,7 @@ def add_base_options(self, compiler):
break
self.coredata.base_options[optname] = oobj

def program_from_cross_file(self, prognames):
def program_from_cross_file(self, prognames, silent=False):
bins = self.environment.cross_info.config['binaries']
for p in prognames:
if hasattr(p, 'held_object'):
Expand All @@ -2236,11 +2262,11 @@ def program_from_cross_file(self, prognames):
raise InterpreterException('Executable name must be a string.')
if p in bins:
exename = bins[p]
extprog = dependencies.ExternalProgram(exename)
extprog = dependencies.ExternalProgram(exename, silent=silent)
progobj = ExternalProgramHolder(extprog)
return progobj

def program_from_system(self, args):
def program_from_system(self, args, silent=False):
# 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.
Expand All @@ -2259,32 +2285,66 @@ def program_from_system(self, args):
else:
raise InvalidArguments('find_program only accepts strings and '
'files, not {!r}'.format(exename))
extprog = dependencies.ExternalProgram(exename, search_dir=search_dir)
extprog = dependencies.ExternalProgram(exename, search_dir=search_dir,
silent=silent)
progobj = ExternalProgramHolder(extprog)
if progobj.found():
return progobj

def program_from_overrides(self, command_names, silent=False):
for name in command_names:
if not isinstance(name, str):
continue
if name in self.build.find_overrides:
exe = self.build.find_overrides[name]
if not silent:
mlog.log('Program', mlog.bold(name), 'found:', mlog.green('YES'),
'(overridden: %s)' % ' '.join(exe.command))
return ExternalProgramHolder(exe)
return None

def store_name_lookups(self, command_names):
for name in command_names:
if isinstance(name, str):
self.build.searched_programs.add(name)

def add_find_program_override(self, name, exe):
if name in self.build.searched_programs:
raise InterpreterException('Tried to override finding of executable "%s" which has already been found.'
% name)
if name in self.build.find_overrides:
raise InterpreterException('Tried to override executable "%s" which has already been overridden.'
% name)
self.build.find_overrides[name] = exe

def find_program_impl(self, args, native=False, required=True, silent=True):
if not isinstance(args, list):
args = [args]
progobj = self.program_from_overrides(args, silent=silent)
if progobj is None and self.build.environment.is_cross_build():
if not native:
progobj = self.program_from_cross_file(args, silent=silent)
if progobj is None:
progobj = self.program_from_system(args, silent=silent)
if required and (progobj is None or not progobj.found()):
raise InvalidArguments('Program(s) {!r} not found or not executable'.format(args))
if progobj is None:
return ExternalProgramHolder(dependencies.NonExistingExternalProgram())
# Only store successful lookups
self.store_name_lookups(args)
return progobj

@permittedKwargs(permitted_kwargs['find_program'])
def func_find_program(self, node, args, kwargs):
if not args:
raise InterpreterException('No program name specified.')
required = kwargs.get('required', True)
if not isinstance(required, bool):
raise InvalidArguments('"required" argument must be a boolean.')
progobj = None
if self.build.environment.is_cross_build():
use_native = kwargs.get('native', False)
if not isinstance(use_native, bool):
raise InvalidArguments('Argument to "native" must be a boolean.')
if not use_native:
progobj = self.program_from_cross_file(args)
if progobj is None:
progobj = self.program_from_system(args)
if required and (progobj is None or not progobj.found()):
raise InvalidArguments('Program(s) {!r} not found or not executable'.format(args))
if progobj is None:
return ExternalProgramHolder(dependencies.NonExistingExternalProgram())
return progobj
use_native = kwargs.get('native', False)
if not isinstance(use_native, bool):
raise InvalidArguments('Argument to "native" must be a boolean.')
return self.find_program_impl(args, native=use_native, required=required, silent=False)

def func_find_library(self, node, args, kwargs):
raise InvalidCode('find_library() is removed, use meson.get_compiler(\'name\').find_library() instead.\n'
Expand Down Expand Up @@ -2691,7 +2751,7 @@ def add_test(self, node, args, kwargs, is_base_test):
exe = args[1]
if not isinstance(exe, (ExecutableHolder, JarHolder, ExternalProgramHolder)):
if isinstance(exe, mesonlib.File):
exe = self.func_find_program(node, (args[1], ), {})
exe = self.func_find_program(node, args[1], {})
else:
raise InterpreterException('Second argument must be executable.')
par = kwargs.get('is_parallel', True)
Expand Down
3 changes: 2 additions & 1 deletion mesonbuild/mesonmain.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ def _generate(self, env):
# Post-conf scripts must be run after writing coredata or else introspection fails.
g.run_postconf_scripts()
except:
os.unlink(cdf)
if 'cdf' in locals():
os.unlink(cdf)
raise

def run_script_command(args):
Expand Down
15 changes: 2 additions & 13 deletions mesonbuild/modules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,15 @@ def wrapped(s, interpreter, state, args, kwargs):
return f(s, interpreter, state, args, kwargs)
return wrapped

_found_programs = {}


class ExtensionModule:
def __init__(self):
def __init__(self, interpreter):
self.interpreter = interpreter
self.snippets = set() # List of methods that operate only on the interpreter.

def is_snippet(self, funcname):
return funcname in self.snippets

def find_program(program_name, target_name):
if program_name in _found_programs:
return _found_programs[program_name]
program = dependencies.ExternalProgram(program_name)
if not program.found():
m = "Target {!r} can't be generated as {!r} could not be found"
raise MesonException(m.format(target_name, program_name))
_found_programs[program_name] = program
return program


def get_include_args(include_dirs, prefix='-I'):
'''
Expand Down
Loading