Fix executable as script on Windows

On Windows this would fail because of missing DLL:
```
mylib = library(...)
exe = executable(..., link_with: mylib)
meson.add_install_script(exe)
```

The reason is on Windows we cannot rely on rpath to find libraries from
build directory, they are searched in $PATH. We already have all that
mechanism in place for custom_target() using ExecutableSerialisation
class, so reuse it for install/dist/postconf scripts too.

This has bonus side effect to also use exe_wrapper for those scripts.

Fixes: #8187
pull/8273/head
Xavier Claessens 4 years ago committed by Jussi Pakkanen
parent c321339b24
commit 0626465ea8
  1. 69
      mesonbuild/backend/backends.py
  2. 8
      mesonbuild/build.py
  3. 15
      mesonbuild/interpreter.py
  4. 9
      mesonbuild/mdist.py
  5. 10
      mesonbuild/minstall.py
  6. 4
      mesonbuild/modules/gnome.py
  7. 7
      mesonbuild/modules/hotdoc.py
  8. 2
      mesonbuild/modules/i18n.py
  9. 26
      mesonbuild/scripts/meson_exe.py
  10. 12
      test cases/common/54 install script/meson.build
  11. 10
      test cases/common/54 install script/prog.c
  12. 10
      test cases/common/54 install script/src/foo.c
  13. 2
      test cases/common/54 install script/src/meson.build

@ -22,9 +22,10 @@ import os
import pickle
import re
import shlex
import subprocess
import textwrap
import typing as T
import hashlib
import copy
from .. import build
from .. import dependencies
@ -105,7 +106,7 @@ class InstallData:
self.data: 'InstallType' = []
self.po_package_name: str = ''
self.po = []
self.install_scripts: T.List[build.RunScript] = []
self.install_scripts: T.List[ExecutableSerialisation] = []
self.install_subdirs: 'InstallSubdirsType' = []
self.mesonintrospect = mesonintrospect
self.version = version
@ -377,13 +378,11 @@ class Backend:
raise MesonException('Unknown data type in object list.')
return obj_list
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):
'''
Serialize an executable for running with a generator or a custom target
'''
import hashlib
def get_executable_serialisation(self, cmd, workdir=None,
extra_bdeps=None, capture=None,
env: T.Optional[build.EnvironmentVariables] = None):
exe = cmd[0]
cmd_args = cmd[1:]
if isinstance(exe, dependencies.ExternalProgram):
exe_cmd = exe.get_command()
exe_for_machine = exe.for_machine
@ -403,11 +402,10 @@ class Backend:
is_cross_built = not self.environment.machines.matches_build_machine(exe_for_machine)
if is_cross_built and self.environment.need_exe_wrapper():
exe_wrapper = self.environment.get_exe_wrapper()
if not exe_wrapper.found():
msg = 'The exe_wrapper {!r} defined in the cross file is ' \
'needed by target {!r}, but was not found. Please ' \
'check the command and/or add it to PATH.'
raise MesonException(msg.format(exe_wrapper.name, tname))
if not exe_wrapper or not exe_wrapper.found():
msg = 'An exe_wrapper is needed but was not found. Please define one ' \
'in cross file and check the command and/or add it to PATH.'
raise MesonException(msg)
else:
if exe_cmd[0].endswith('.jar'):
exe_cmd = ['java', '-jar'] + exe_cmd
@ -415,11 +413,24 @@ class Backend:
exe_cmd = ['mono'] + exe_cmd
exe_wrapper = None
workdir = workdir or self.environment.get_build_dir()
return ExecutableSerialisation(exe_cmd + cmd_args, env,
exe_wrapper, workdir,
extra_paths, capture)
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):
'''
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)
reasons = []
if extra_paths:
if es.extra_paths:
reasons.append('to set PATH')
if exe_wrapper:
if es.exe_runner:
reasons.append('to use exe_wrapper')
if workdir:
@ -440,10 +451,9 @@ class Backend:
if not capture:
return None, ''
return ((self.environment.get_build_command() +
['--internal', 'exe', '--capture', capture, '--'] + exe_cmd + cmd_args),
['--internal', 'exe', '--capture', capture, '--'] + es.cmd_args),
', '.join(reasons))
workdir = workdir or self.environment.get_build_dir()
if isinstance(exe, (dependencies.ExternalProgram,
build.BuildTarget, build.CustomTarget)):
basename = exe.name
@ -454,15 +464,12 @@ class Backend:
# Take a digest of the cmd args, env, workdir, and capture. This avoids
# collisions and also makes the name deterministic over regenerations
# which avoids a rebuild by Ninja because the cmdline stays the same.
data = bytes(str(env) + str(cmd_args) + str(workdir) + str(capture),
data = bytes(str(env) + str(cmd_args) + str(es.workdir) + str(capture),
encoding='utf-8')
digest = hashlib.sha1(data).hexdigest()
scratch_file = 'meson_exe_{0}_{1}.dat'.format(basename, digest)
exe_data = os.path.join(self.environment.get_scratch_dir(), scratch_file)
with open(exe_data, 'wb') as f:
es = ExecutableSerialisation(exe_cmd + cmd_args, env,
exe_wrapper, workdir,
extra_paths, capture)
pickle.dump(es, f)
return (self.environment.get_build_command() + ['--internal', 'exe', '--unpickle', exe_data],
', '.join(reasons))
@ -1186,16 +1193,16 @@ class Backend:
return inputs, outputs, cmd
def run_postconf_scripts(self) -> None:
from ..scripts.meson_exe import run_exe
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']]),
}
child_env = os.environ.copy()
child_env.update(env)
for s in self.build.postconf_scripts:
cmd = s['exe'] + s['args']
subprocess.check_call(cmd, env=child_env)
name = ' '.join(s.cmd_args)
mlog.log('Running postconf script {!r}'.format(name))
run_exe(s, env)
def create_install_data(self) -> InstallData:
strip_bin = self.environment.lookup_binary_entry(MachineChoice.HOST, 'strip')
@ -1324,18 +1331,18 @@ class Backend:
d.targets.append(i)
def generate_custom_install_script(self, d: InstallData) -> None:
result: T.List[build.RunScript] = []
result: T.List[ExecutableSerialisation] = []
srcdir = self.environment.get_source_dir()
builddir = self.environment.get_build_dir()
for i in self.build.install_scripts:
exe = i['exe']
args = i['args']
fixed_args = []
for a in args:
for a in i.cmd_args:
a = a.replace('@SOURCE_ROOT@', srcdir)
a = a.replace('@BUILD_ROOT@', builddir)
fixed_args.append(a)
result.append(build.RunScript(exe, fixed_args))
es = copy.copy(i)
es.cmd_args = fixed_args
result.append(es)
d.install_scripts = result
def generate_header_install(self, d: InstallData) -> None:

@ -2609,14 +2609,6 @@ class Data:
else:
self.rename = rename
class RunScript(dict):
def __init__(self, script, args):
super().__init__()
assert(isinstance(script, list))
assert(isinstance(args, list))
self['exe'] = script
self['args'] = args
class TestSetup:
def __init__(self, exe_wrapper: T.Optional[T.List[str]], gdb: bool,
timeout_multiplier: int, env: EnvironmentVariables):

@ -34,7 +34,7 @@ from .interpreterbase import ObjectHolder, MesonVersionString
from .interpreterbase import TYPE_var, TYPE_nkwargs
from .modules import ModuleReturnValue, ExtensionModule
from .cmake import CMakeInterpreter
from .backend.backends import TestProtocol, Backend
from .backend.backends import TestProtocol, Backend, ExecutableSerialisation
from pathlib import Path, PurePath
import os
@ -1948,11 +1948,8 @@ class MesonMain(InterpreterObject):
})
def _find_source_script(self, prog: T.Union[str, mesonlib.File, ExecutableHolder], args):
if isinstance(prog, ExecutableHolder):
prog_path = self.interpreter.backend.get_target_filename(prog.held_object)
return build.RunScript([prog_path], args)
elif isinstance(prog, ExternalProgramHolder):
return build.RunScript(prog.get_command(), args)
if isinstance(prog, (ExecutableHolder, ExternalProgramHolder)):
return self.interpreter.backend.get_executable_serialisation([unholder(prog)] + args)
# Prefer scripts in the current source directory
search_dir = os.path.join(self.interpreter.environment.source_dir,
self.interpreter.subdir)
@ -1970,7 +1967,7 @@ class MesonMain(InterpreterObject):
else:
m = 'Script or command {!r} not found or not executable'
raise InterpreterException(m.format(prog))
return build.RunScript(found.get_command(), args)
return self.interpreter.backend.get_executable_serialisation([found] + args)
def _process_script_args(
self, name: str, args: T.List[T.Union[
@ -2557,7 +2554,7 @@ class Interpreter(InterpreterBase):
return GeneratedListHolder(item)
elif isinstance(item, build.RunTarget):
raise RuntimeError('This is not a pipe.')
elif isinstance(item, build.RunScript):
elif isinstance(item, ExecutableSerialisation):
raise RuntimeError('Do not do this.')
elif isinstance(item, build.Data):
return DataHolder(item)
@ -2584,7 +2581,7 @@ class Interpreter(InterpreterBase):
self.module_method_callback(v)
elif isinstance(v, build.GeneratedList):
pass
elif isinstance(v, build.RunScript):
elif isinstance(v, ExecutableSerialisation):
self.build.install_scripts.append(v)
elif isinstance(v, build.Data):
self.build.data.append(v)

@ -26,6 +26,7 @@ from mesonbuild.environment import detect_ninja
from mesonbuild.mesonlib import windows_proof_rmtree, MesonException, quiet_git
from mesonbuild.wrap import wrap
from mesonbuild import mlog, build
from .scripts.meson_exe import run_exe
archive_choices = ['gztar', 'xztar', 'zip']
archive_extension = {'gztar': '.tar.gz',
@ -79,17 +80,15 @@ def process_submodules(dirname):
def run_dist_scripts(src_root, bld_root, dist_root, dist_scripts):
assert(os.path.isabs(dist_root))
env = os.environ.copy()
env = {}
env['MESON_DIST_ROOT'] = dist_root
env['MESON_SOURCE_ROOT'] = src_root
env['MESON_BUILD_ROOT'] = bld_root
for d in dist_scripts:
script = d['exe']
args = d['args']
name = ' '.join(script + args)
name = ' '.join(d.cmd_args)
print('Running custom dist script {!r}'.format(name))
try:
rc = subprocess.call(script + args, env=env)
rc = run_exe(d, env)
if rc != 0:
sys.exit('Dist script errored out')
except OSError:

@ -30,6 +30,7 @@ from .coredata import major_versions_differ, MesonVersionMismatchException
from .coredata import version as coredata_version
from .mesonlib import is_windows, Popen_safe
from .scripts import depfixer, destdir_join
from .scripts.meson_exe import run_exe
try:
from __main__ import __file__ as main_file
except ImportError:
@ -485,17 +486,12 @@ class Installer:
if self.options.quiet:
env['MESON_INSTALL_QUIET'] = '1'
child_env = os.environ.copy()
child_env.update(env)
for i in d.install_scripts:
self.did_install_something = True # Custom script must report itself if it does nothing.
script = i['exe']
args = i['args']
name = ' '.join(script + args)
name = ' '.join(i.cmd_args)
self.log('Running custom install script {!r}'.format(name))
try:
rc = subprocess.call(script + args, env=child_env)
rc = run_exe(i, env)
except OSError:
print('FAILED: install script \'{}\' could not be run, stopped'.format(name))
# POSIX shells return 127 when a command could not be found

@ -899,7 +899,7 @@ class GnomeModule(ExtensionModule):
args.append('--media=' + '@@'.join(media))
if langs:
args.append('--langs=' + '@@'.join(langs))
inscript = build.RunScript(script, args)
inscript = state.backend.get_executable_serialisation(script + args)
potargs = state.environment.get_build_command() + [
'--internal', 'yelphelper', 'pot',
@ -1051,7 +1051,7 @@ class GnomeModule(ExtensionModule):
self.interpreter.add_test(state.current_node, check_args, check_kwargs, True)
res = [custom_target, alias_target]
if kwargs.get('install', True):
res.append(build.RunScript(command, args))
res.append(state.backend.get_executable_serialisation(command + args))
return ModuleReturnValue(custom_target, res)
def _get_build_args(self, kwargs, state, depends):

@ -350,7 +350,7 @@ class HotdocTargetBuilder:
install_script = None
if install is True:
install_script = HotdocRunScript(self.build_command, [
install_script = self.state.backend.get_executable_serialisation(self.build_command + [
"--internal", "hotdoc",
"--install", os.path.join(fullname, 'html'),
'--name', self.name,
@ -391,11 +391,6 @@ class HotdocTarget(build.CustomTarget):
return res
class HotdocRunScript(build.RunScript):
def __init__(self, script, args):
super().__init__(script, args)
class HotDocModule(ExtensionModule):
@FeatureNew('Hotdoc Module', '0.48.0')
def __init__(self, interpreter):

@ -180,7 +180,7 @@ class I18nModule(ExtensionModule):
pkg_arg]
if lang_arg:
args.append(lang_arg)
iscript = build.RunScript(script, args)
iscript = state.backend.get_executable_serialisation(script + args)
targets.append(iscript)
return ModuleReturnValue(None, targets)

@ -30,7 +30,7 @@ def buildparser() -> argparse.ArgumentParser:
parser.add_argument('--capture')
return parser
def run_exe(exe: ExecutableSerialisation) -> int:
def run_exe(exe: ExecutableSerialisation, extra_env: T.Optional[dict] = None) -> int:
if exe.exe_runner:
if not exe.exe_runner.found():
raise AssertionError('BUG: Can\'t run cross-compiled exe {!r} with not-found '
@ -39,6 +39,8 @@ def run_exe(exe: ExecutableSerialisation) -> int:
else:
cmd_args = exe.cmd_args
child_env = os.environ.copy()
if extra_env:
child_env.update(extra_env)
if exe.env:
child_env = exe.env.get_env(child_env)
if exe.extra_paths:
@ -56,14 +58,21 @@ def run_exe(exe: ExecutableSerialisation) -> int:
stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if exe.pickled and p.returncode != 0:
print('while executing {!r}'.format(cmd_args))
if p.returncode == 0xc0000135:
# STATUS_DLL_NOT_FOUND on Windows indicating a common problem that is otherwise hard to diagnose
raise FileNotFoundError('due to missing DLLs')
if exe.capture and p.returncode == 0:
if p.returncode != 0:
if exe.pickled:
print('while executing {!r}'.format(cmd_args))
if not exe.capture:
print('--- stdout ---')
print(stdout.decode())
print('--- stderr ---')
print(stderr.decode())
return p.returncode
if exe.capture:
skip_write = False
try:
with open(exe.capture, 'rb') as cur:
@ -73,11 +82,8 @@ def run_exe(exe: ExecutableSerialisation) -> int:
if not skip_write:
with open(exe.capture, 'wb') as output:
output.write(stdout)
else:
sys.stdout.buffer.write(stdout)
if stderr:
sys.stderr.buffer.write(stderr)
return p.returncode
return 0
def run(args: T.List[str]) -> int:
global options

@ -1,6 +1,5 @@
project('custom install script', 'c')
executable('prog', 'prog.c', install : true)
meson.add_install_script('myinstall.py', 'diiba/daaba', 'file.dat')
meson.add_install_script('myinstall.py', 'this/should', 'also-work.dat')
@ -33,3 +32,14 @@ installer = configure_file(
)
meson.add_install_script(installer, 'otherdir', afile, '--mode=copy')
# This executable links on a library built in src/ directory. On Windows this
# means meson must add src/ into $PATH to find the DLL when running it as
# install script.
myexe = executable('prog', 'prog.c',
link_with: mylib,
install : true,
)
if meson.can_run_host_binaries()
meson.add_install_script(myexe)
endif

@ -1,6 +1,14 @@
#include<stdio.h>
#ifdef _WIN32
#define DO_IMPORT __declspec(dllimport)
#else
#define DO_IMPORT
#endif
DO_IMPORT int foo(void);
int main(void) {
printf("This is text.\n");
return 0;
return foo();
}

@ -0,0 +1,10 @@
#ifdef _WIN32
#define DO_EXPORT __declspec(dllexport)
#else
#define DO_EXPORT
#endif
DO_EXPORT int foo(void)
{
return 0;
}

@ -1,3 +1,5 @@
meson.add_install_script('myinstall.py', 'this/does', 'something-different.dat')
afile = files('a file.txt')
mylib = shared_library('mylib', 'foo.c')

Loading…
Cancel
Save