run_target: Add env kwarg

Re-implement it in backend using the same code path as for
custom_target(). This for example handle setting PATH on Windows when
command is an executable.
pull/8305/head
Xavier Claessens 4 years ago
parent 5d94d161ff
commit 522392e755
  1. 4
      docs/markdown/Reference-manual.md
  2. 2
      docs/markdown/snippets/customtarget_env.md
  3. 20
      mesonbuild/backend/backends.py
  4. 76
      mesonbuild/backend/ninjabackend.py
  5. 36
      mesonbuild/backend/vs2010backend.py
  6. 72
      mesonbuild/build.py
  7. 7
      mesonbuild/interpreter.py
  8. 8
      mesonbuild/modules/gnome.py
  9. 6
      mesonbuild/modules/i18n.py
  10. 84
      mesonbuild/scripts/commandrunner.py
  11. 11
      mesonbuild/scripts/meson_exe.py
  12. 1
      run_unittests.py
  13. 9
      test cases/common/52 run target/check-env.py
  14. 6
      test cases/common/52 run target/meson.build

@ -1504,6 +1504,10 @@ and subdirectory the target was defined in, respectively.
- `depends` is a list of targets that this target depends on but which
are not listed in the command array (because, for example, the
script does file globbing internally)
- `env` *(since 0.57.0)*: environment variables to set, such as
`{'NAME1': 'value1', 'NAME2': 'value2'}` or `['NAME1=value1', 'NAME2=value2']`,
or an [`environment()` object](#environment-object) which allows more
sophisticated environment juggling.
### set_variable()

@ -1,4 +1,4 @@
## `custom_target()` now accepts an `env` keyword argument
## `custom_target()` and `run_target()` now accepts an `env` keyword argument
Environment variables can now be passed to the `custom_target()` command.

@ -21,7 +21,6 @@ import json
import os
import pickle
import re
import shlex
import textwrap
import typing as T
import hashlib
@ -34,7 +33,7 @@ from .. import mlog
from ..compilers import LANGUAGES_USING_LDFLAGS
from ..mesonlib import (
File, MachineChoice, MesonException, OptionType, OrderedSet, OptionOverrideProxy,
classify_unity_sources, unholder, OptionKey
classify_unity_sources, unholder, OptionKey, join_args
)
if T.TYPE_CHECKING:
@ -138,6 +137,7 @@ class ExecutableSerialisation:
self.capture = capture
self.pickled = False
self.skip_if_destdir = False
self.verbose = False
class TestSerialisation:
def __init__(self, name: str, project: str, suite: str, fname: T.List[str],
@ -421,12 +421,14 @@ class Backend:
def as_meson_exe_cmdline(self, tname, exe, cmd_args, workdir=None,
extra_bdeps=None, capture=None, force_serialize=False,
env: T.Optional[build.EnvironmentVariables] = None):
env: T.Optional[build.EnvironmentVariables] = None,
verbose: bool = False):
'''
Serialize an executable for running with a generator or a custom target
'''
cmd = [exe] + cmd_args
es = self.get_executable_serialisation(cmd, workdir, extra_bdeps, capture, env)
es.verbose = verbose
reasons = []
if es.extra_paths:
reasons.append('to set PATH')
@ -1193,11 +1195,21 @@ class Backend:
cmd = [i.replace('\\', '/') for i in cmd]
return inputs, outputs, cmd
def get_run_target_env(self, target: build.RunTarget) -> build.EnvironmentVariables:
env = target.env if target.env else build.EnvironmentVariables()
introspect_cmd = join_args(self.environment.get_build_command() + ['introspect'])
env.add_var(env.set, 'MESON_SOURCE_ROOT', [self.environment.get_source_dir()], {})
env.add_var(env.set, 'MESON_BUILD_ROOT', [self.environment.get_build_dir()], {})
env.add_var(env.set, 'MESON_SUBDIR', [target.subdir], {})
env.add_var(env.set, 'MESONINTROSPECT', [introspect_cmd], {})
return env
def run_postconf_scripts(self) -> None:
from ..scripts.meson_exe import run_exe
introspect_cmd = join_args(self.environment.get_build_command() + ['introspect'])
env = {'MESON_SOURCE_ROOT': self.environment.get_source_dir(),
'MESON_BUILD_ROOT': self.environment.get_build_dir(),
'MESONINTROSPECT': ' '.join([shlex.quote(x) for x in self.environment.get_build_command() + ['introspect']]),
'MESONINTROSPECT': introspect_cmd,
}
for s in self.build.postconf_scripts:

@ -28,7 +28,6 @@ from .. import modules
from .. import environment, mesonlib
from .. import build
from .. import mlog
from .. import dependencies
from .. import compilers
from ..arglist import CompilerArgs
from ..compilers import (
@ -989,65 +988,28 @@ int dummy;
return '{}{}'.format(subproject_prefix, target.name)
def generate_run_target(self, target):
cmd = self.environment.get_build_command() + ['--internal', 'commandrunner']
deps = self.unwrap_dep_list(target)
arg_strings = []
for i in target.args:
if isinstance(i, str):
arg_strings.append(i)
elif isinstance(i, (build.BuildTarget, build.CustomTarget)):
relfname = self.get_target_filename(i)
arg_strings.append(os.path.join(self.environment.get_build_dir(), relfname))
deps.append(relfname)
elif isinstance(i, mesonlib.File):
relfname = i.rel_to_builddir(self.build_to_src)
arg_strings.append(os.path.join(self.environment.get_build_dir(), relfname))
else:
raise AssertionError('Unreachable code in generate_run_target: ' + str(i))
cmd += [self.environment.get_source_dir(),
self.environment.get_build_dir(),
target.subdir] + self.environment.get_build_command()
texe = target.command
try:
texe = texe.held_object
except AttributeError:
pass
if isinstance(texe, build.Executable):
abs_exe = os.path.join(self.environment.get_build_dir(), self.get_target_filename(texe))
deps.append(self.get_target_filename(texe))
if self.environment.is_cross_build():
exe_wrap = self.environment.get_exe_wrapper()
if exe_wrap:
if not exe_wrap.found():
msg = 'The exe_wrapper {!r} defined in the cross file is ' \
'needed by run target {!r}, but was not found. ' \
'Please check the command and/or add it to PATH.'
raise MesonException(msg.format(exe_wrap.name, target.name))
cmd += exe_wrap.get_command()
cmd.append(abs_exe)
elif isinstance(texe, dependencies.ExternalProgram):
cmd += texe.get_command()
elif isinstance(texe, build.CustomTarget):
deps.append(self.get_target_filename(texe))
cmd += [os.path.join(self.environment.get_build_dir(), self.get_target_filename(texe))]
elif isinstance(texe, mesonlib.File):
cmd.append(texe.absolute_path(self.environment.get_source_dir(), self.environment.get_build_dir()))
target_name = self.build_run_target_name(target)
if not target.command:
# This is an alias target, it has no command, it just depends on
# other targets.
elem = NinjaBuildElement(self.all_outputs, target_name, 'phony', [])
else:
cmd.append(target.command)
cmd += arg_strings
if texe:
target_name = 'meson-{}'.format(self.build_run_target_name(target))
elem = NinjaBuildElement(self.all_outputs, target_name, 'CUSTOM_COMMAND', [])
elem.add_item('COMMAND', cmd)
elem.add_item('description', 'Running external command {}'.format(target.name))
target_env = self.get_run_target_env(target)
_, _, cmd = self.eval_custom_target_command(target)
desc = 'Running external command {}{}'
meson_exe_cmd, reason = self.as_meson_exe_cmdline(target_name, cmd[0], cmd[1:],
force_serialize=True, env=target_env,
verbose=True)
cmd_type = ' (wrapped by meson {})'.format(reason)
internal_target_name = 'meson-{}'.format(target_name)
elem = NinjaBuildElement(self.all_outputs, internal_target_name, 'CUSTOM_COMMAND', [])
elem.add_item('COMMAND', meson_exe_cmd)
elem.add_item('description', desc.format(target.name, cmd_type))
elem.add_item('pool', 'console')
# Alias that runs the target defined above with the name the user specified
self.create_target_alias(target_name)
else:
target_name = self.build_run_target_name(target)
elem = NinjaBuildElement(self.all_outputs, target_name, 'phony', [])
self.create_target_alias(internal_target_name)
deps = self.unwrap_dep_list(target)
deps += self.get_custom_target_depend_files(target)
elem.add_dep(deps)
self.add_build(elem)
self.processed_targets[target.get_id()] = True

@ -28,7 +28,7 @@ from .. import mlog
from .. import compilers
from ..interpreter import Interpreter
from ..mesonlib import (
MesonException, File, python_command, replace_if_different, OptionKey,
MesonException, python_command, replace_if_different, OptionKey,
)
from ..environment import Environment, build_filename
@ -257,9 +257,8 @@ class Vs2010Backend(backends.Backend):
for d in target.get_target_dependencies():
all_deps[d.get_id()] = d
elif isinstance(target, build.RunTarget):
for d in [target.command] + target.args:
if isinstance(d, (build.BuildTarget, build.CustomTarget)):
all_deps[d.get_id()] = d
for d in target.get_dependencies():
all_deps[d.get_id()] = d
elif isinstance(target, build.BuildTarget):
for ldep in target.link_targets:
if isinstance(ldep, build.CustomTargetIndex):
@ -534,27 +533,14 @@ class Vs2010Backend(backends.Backend):
# is probably a better way than running a this dummy command.
cmd_raw = python_command + ['-c', 'exit']
else:
cmd_raw = [target.command] + target.args
cmd = python_command + \
[os.path.join(self.environment.get_script_dir(), 'commandrunner.py'),
self.environment.get_source_dir(),
self.environment.get_build_dir(),
self.get_target_dir(target)] + self.environment.get_build_command()
for i in cmd_raw:
if isinstance(i, build.BuildTarget):
cmd.append(os.path.join(self.environment.get_build_dir(), self.get_target_filename(i)))
elif isinstance(i, dependencies.ExternalProgram):
cmd += i.get_command()
elif isinstance(i, File):
relfname = i.rel_to_builddir(self.build_to_src)
cmd.append(os.path.join(self.environment.get_build_dir(), relfname))
elif isinstance(i, str):
# Escape embedded quotes, because we quote the entire argument below.
cmd.append(i.replace('"', '\\"'))
else:
cmd.append(i)
cmd_templ = '''"%s" ''' * len(cmd)
self.add_custom_build(root, 'run_target', cmd_templ % tuple(cmd))
_, _, cmd_raw = self.eval_custom_target_command(target)
depend_files = self.get_custom_target_depend_files(target)
target_env = self.get_run_target_env(target)
wrapper_cmd, _ = self.as_meson_exe_cmdline(target.name, cmd_raw[0], cmd_raw[1:],
force_serialize=True, env=target_env,
verbose=True)
self.add_custom_build(root, 'run_target', ' '.join(self.quote_arguments(wrapper_cmd)),
deps=depend_files)
ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets')
self.add_regen_dependency(root)
self.add_target_deps(root, target)

@ -29,7 +29,7 @@ from .mesonlib import (
File, MesonException, MachineChoice, PerMachine, OrderedSet, listify,
extract_as_list, typeslistify, stringlistify, classify_unity_sources,
get_filenames_templates_dict, substitute_values, has_path_sep, unholder,
OptionKey,
OptionKey
)
from .compilers import (
Compiler, is_object, clink_langs, sort_clink, lang_suffixes,
@ -2142,8 +2142,35 @@ class SharedModule(SharedLibrary):
def get_default_install_dir(self, environment):
return environment.get_shared_module_dir()
class CommandBase:
def flatten_command(self, cmd):
cmd = unholder(listify(cmd))
final_cmd = []
for c in cmd:
if isinstance(c, str):
final_cmd.append(c)
elif isinstance(c, File):
self.depend_files.append(c)
final_cmd.append(c)
elif isinstance(c, dependencies.ExternalProgram):
if not c.found():
raise InvalidArguments('Tried to use not-found external program in "command"')
path = c.get_path()
if os.path.isabs(path):
# Can only add a dependency on an external program which we
# know the absolute path of
self.depend_files.append(File.from_absolute_file(path))
final_cmd += c.get_command()
elif isinstance(c, (BuildTarget, CustomTarget)):
self.dependencies.append(c)
final_cmd.append(c)
elif isinstance(c, list):
final_cmd += self.flatten_command(c)
else:
raise InvalidArguments('Argument {!r} in "command" is invalid'.format(c))
return final_cmd
class CustomTarget(Target):
class CustomTarget(Target, CommandBase):
known_kwargs = set([
'input',
'output',
@ -2214,33 +2241,6 @@ class CustomTarget(Target):
bdeps.update(d.get_transitive_build_target_deps())
return bdeps
def flatten_command(self, cmd):
cmd = unholder(listify(cmd))
final_cmd = []
for c in cmd:
if isinstance(c, str):
final_cmd.append(c)
elif isinstance(c, File):
self.depend_files.append(c)
final_cmd.append(c)
elif isinstance(c, dependencies.ExternalProgram):
if not c.found():
raise InvalidArguments('Tried to use not-found external program in "command"')
path = c.get_path()
if os.path.isabs(path):
# Can only add a dependency on an external program which we
# know the absolute path of
self.depend_files.append(File.from_absolute_file(path))
final_cmd += c.get_command()
elif isinstance(c, (BuildTarget, CustomTarget)):
self.dependencies.append(c)
final_cmd.append(c)
elif isinstance(c, list):
final_cmd += self.flatten_command(c)
else:
raise InvalidArguments('Argument {!r} in "command" is invalid'.format(c))
return final_cmd
def process_kwargs(self, kwargs, backend):
self.process_kwargs_base(kwargs)
self.sources = unholder(extract_as_list(kwargs, 'input'))
@ -2422,18 +2422,20 @@ class CustomTarget(Target):
for i in self.outputs:
yield CustomTargetIndex(self, i)
class RunTarget(Target):
def __init__(self, name, command, args, dependencies, subdir, subproject):
class RunTarget(Target, CommandBase):
def __init__(self, name, command, dependencies, subdir, subproject, env=None):
self.typename = 'run'
# These don't produce output artifacts
super().__init__(name, subdir, subproject, False, MachineChoice.BUILD)
self.command = command
self.args = args
self.dependencies = dependencies
self.depend_files = []
self.command = self.flatten_command(command)
self.absolute_paths = False
self.env = env
def __repr__(self):
repr_str = "<{0} {1}: {2}>"
return repr_str.format(self.__class__.__name__, self.get_id(), self.command)
return repr_str.format(self.__class__.__name__, self.get_id(), self.command[0])
def process_kwargs(self, kwargs):
return self.process_kwargs_base(kwargs)
@ -2466,7 +2468,7 @@ class RunTarget(Target):
class AliasTarget(RunTarget):
def __init__(self, name, dependencies, subdir, subproject):
super().__init__(name, '', [], dependencies, subdir, subproject)
super().__init__(name, [], dependencies, subdir, subproject)
class Jar(BuildTarget):
known_kwargs = known_jar_kwargs

@ -2377,7 +2377,7 @@ permitted_kwargs = {'add_global_arguments': {'language', 'native'},
'jar': build.known_jar_kwargs,
'project': {'version', 'meson_version', 'default_options', 'license', 'subproject_dir'},
'run_command': {'check', 'capture', 'env'},
'run_target': {'command', 'depends'},
'run_target': {'command', 'depends', 'env'},
'shared_library': build.known_shlib_kwargs,
'shared_module': build.known_shmod_kwargs,
'static_library': build.known_stlib_kwargs,
@ -4059,6 +4059,7 @@ This will become a hard error in the future.''' % kwargs['input'], location=self
self.add_target(name, tg.held_object)
return tg
@FeatureNewKwargs('run_target', '0.57.0', ['env'])
@permittedKwargs(permitted_kwargs['run_target'])
def func_run_target(self, node, args, kwargs):
if len(args) > 1:
@ -4087,8 +4088,8 @@ This will become a hard error in the future.''' % kwargs['input'], location=self
if not isinstance(d, (build.BuildTarget, build.CustomTarget)):
raise InterpreterException('Depends items must be build targets.')
cleaned_deps.append(d)
command, *cmd_args = cleaned_args
tg = RunTargetHolder(build.RunTarget(name, command, cmd_args, cleaned_deps, self.subdir, self.subproject), self)
env = self.unpack_env_kwarg(kwargs)
tg = RunTargetHolder(build.RunTarget(name, cleaned_args, cleaned_deps, self.subdir, self.subproject, env), self)
self.add_target(name, tg.held_object)
full_name = (self.subproject, name)
assert(full_name not in self.build.run_target_names)

@ -907,8 +907,8 @@ class GnomeModule(ExtensionModule):
'--id=' + project_id,
'--sources=' + source_str,
]
pottarget = build.RunTarget('help-' + project_id + '-pot', potargs[0],
potargs[1:], [], state.subdir, state.subproject)
pottarget = build.RunTarget('help-' + project_id + '-pot', potargs,
[], state.subdir, state.subproject)
poargs = state.environment.get_build_command() + [
'--internal', 'yelphelper', 'update-po',
@ -917,8 +917,8 @@ class GnomeModule(ExtensionModule):
'--sources=' + source_str,
'--langs=' + '@@'.join(langs),
]
potarget = build.RunTarget('help-' + project_id + '-update-po', poargs[0],
poargs[1:], [], state.subdir, state.subproject)
potarget = build.RunTarget('help-' + project_id + '-update-po', poargs,
[], state.subdir, state.subproject)
rv = [inscript, pottarget, potarget]
return ModuleReturnValue(None, rv)

@ -152,12 +152,12 @@ class I18nModule(ExtensionModule):
potargs.append(datadirs)
if extra_args:
potargs.append(extra_args)
pottarget = build.RunTarget(packagename + '-pot', potargs[0], potargs[1:], [], state.subdir, state.subproject)
pottarget = build.RunTarget(packagename + '-pot', potargs, [], state.subdir, state.subproject)
gmoargs = state.environment.get_build_command() + ['--internal', 'gettext', 'gen_gmo']
if lang_arg:
gmoargs.append(lang_arg)
gmotarget = build.RunTarget(packagename + '-gmo', gmoargs[0], gmoargs[1:], [], state.subdir, state.subproject)
gmotarget = build.RunTarget(packagename + '-gmo', gmoargs, [], state.subdir, state.subproject)
updatepoargs = state.environment.get_build_command() + ['--internal', 'gettext', 'update_po', pkg_arg]
if lang_arg:
@ -166,7 +166,7 @@ class I18nModule(ExtensionModule):
updatepoargs.append(datadirs)
if extra_args:
updatepoargs.append(extra_args)
updatepotarget = build.RunTarget(packagename + '-update-po', updatepoargs[0], updatepoargs[1:], [], state.subdir, state.subproject)
updatepotarget = build.RunTarget(packagename + '-update-po', updatepoargs, [], state.subdir, state.subproject)
targets = [pottarget, gmotarget, updatepotarget]

@ -1,84 +0,0 @@
# Copyright 2014 The Meson development team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This program is a wrapper to run external commands. It determines
what to run, sets up the environment and executes the command."""
import sys, os, subprocess, shutil, shlex
import re
import typing as T
def run_command(source_dir: str, build_dir: str, subdir: str, meson_command: T.List[str], command: str, arguments: T.List[str]) -> subprocess.Popen:
env = {'MESON_SOURCE_ROOT': source_dir,
'MESON_BUILD_ROOT': build_dir,
'MESON_SUBDIR': subdir,
'MESONINTROSPECT': ' '.join([shlex.quote(x) for x in meson_command + ['introspect']]),
}
cwd = os.path.join(source_dir, subdir)
child_env = os.environ.copy()
child_env.update(env)
# Is the command an executable in path?
exe = shutil.which(command)
if exe is not None:
command_array = [exe] + arguments
else:# No? Maybe it is a script in the source tree.
fullpath = os.path.join(source_dir, subdir, command)
command_array = [fullpath] + arguments
try:
return subprocess.Popen(command_array, env=child_env, cwd=cwd)
except FileNotFoundError:
print('Could not execute command "%s". File not found.' % command)
sys.exit(1)
except PermissionError:
print('Could not execute command "%s". File not executable.' % command)
sys.exit(1)
except OSError as err:
print('Could not execute command "{}": {}'.format(command, err))
sys.exit(1)
except subprocess.SubprocessError as err:
print('Could not execute command "{}": {}'.format(command, err))
sys.exit(1)
def is_python_command(cmdname: str) -> bool:
end_py_regex = r'python(3|3\.\d+)?(\.exe)?$'
return re.search(end_py_regex, cmdname) is not None
def run(args: T.List[str]) -> int:
if len(args) < 4:
print('commandrunner.py <source dir> <build dir> <subdir> <command> [arguments]')
return 1
src_dir = args[0]
build_dir = args[1]
subdir = args[2]
meson_bin = args[3]
if is_python_command(meson_bin):
meson_command = [meson_bin, args[4]]
command = args[5]
arguments = args[6:]
else:
meson_command = [meson_bin]
command = args[4]
arguments = args[5:]
pc = run_command(src_dir, build_dir, subdir, meson_command, command, arguments)
while True:
try:
pc.wait()
break
except KeyboardInterrupt:
pass
return pc.returncode
if __name__ == '__main__':
sys.exit(run(sys.argv[1:]))

@ -52,10 +52,13 @@ def run_exe(exe: ExecutableSerialisation, extra_env: T.Optional[dict] = None) ->
['Z:' + p for p in exe.extra_paths] + child_env.get('WINEPATH', '').split(';')
)
pipe = subprocess.PIPE
if exe.verbose:
assert not exe.capture, 'Cannot capture and print to console at the same time'
pipe = None
p = subprocess.Popen(cmd_args, env=child_env, cwd=exe.workdir,
close_fds=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
close_fds=False, stdout=pipe, stderr=pipe)
stdout, stderr = p.communicate()
if p.returncode == 0xc0000135:
@ -65,6 +68,8 @@ def run_exe(exe: ExecutableSerialisation, extra_env: T.Optional[dict] = None) ->
if p.returncode != 0:
if exe.pickled:
print('while executing {!r}'.format(cmd_args))
if exe.verbose:
return p.returncode
if not exe.capture:
print('--- stdout ---')
print(stdout.decode())

@ -2192,6 +2192,7 @@ class AllPlatformTests(BasePlatformTests):
testdir = os.path.join(self.common_test_dir, '52 run target')
self.init(testdir)
self.run_target('check_exists')
self.run_target('check-env')
def test_install_introspection(self):
'''

@ -0,0 +1,9 @@
#!/usr/bin/env python3
import os
assert 'MESON_SOURCE_ROOT' in os.environ
assert 'MESON_BUILD_ROOT' in os.environ
assert 'MESON_SUBDIR' in os.environ
assert 'MESONINTROSPECT' in os.environ
assert 'MY_ENV' in os.environ

@ -72,3 +72,9 @@ run_target('ctags',
run_target('clang-format',
command : converter)
# Check we can pass env to the program
run_target('check-env',
command: [find_program('check-env.py')],
env: {'MY_ENV': '1'},
)

Loading…
Cancel
Save