diff options
Diffstat (limited to 'mesonbuild/backend/backends.py')
-rw-r--r-- | mesonbuild/backend/backends.py | 253 |
1 files changed, 170 insertions, 83 deletions
diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index 926a07d..86d20f7 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -12,24 +12,54 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os, pickle, re +from collections import OrderedDict +from functools import lru_cache +from pathlib import Path +import enum +import json +import os +import pickle +import re +import shlex +import subprocess import textwrap +import typing as T + from .. import build from .. import dependencies from .. import mesonlib from .. import mlog -import json -import subprocess from ..mesonlib import ( File, Language, MachineChoice, MesonException, OrderedSet, OptionOverrideProxy, classify_unity_sources, unholder, ) -from ..compilers import CompilerArgs, VisualStudioLikeCompiler -from ..interpreter import Interpreter -from collections import OrderedDict -import shlex -from functools import lru_cache -import typing as T + +if T.TYPE_CHECKING: + from ..interpreter import Interpreter + + +class TestProtocol(enum.Enum): + + EXITCODE = 0 + TAP = 1 + GTEST = 2 + + @classmethod + def from_str(cls, string: str) -> 'TestProtocol': + if string == 'exitcode': + return cls.EXITCODE + elif string == 'tap': + return cls.TAP + elif string == 'gtest': + return cls.GTEST + raise MesonException('unknown test format {}'.format(string)) + + def __str__(self) -> str: + if self is self.EXITCODE: + return 'exitcode' + elif self is self.GTEST: + return 'gtest' + return 'tap' class CleanTrees: @@ -60,12 +90,13 @@ class InstallData: self.mesonintrospect = mesonintrospect class TargetInstallData: - def __init__(self, fname, outdir, aliases, strip, install_name_mappings, install_rpath, install_mode, optional=False): + def __init__(self, fname, outdir, aliases, strip, install_name_mappings, rpath_dirs_to_remove, install_rpath, install_mode, optional=False): self.fname = fname self.outdir = outdir self.aliases = aliases self.strip = strip self.install_name_mappings = install_name_mappings + self.rpath_dirs_to_remove = rpath_dirs_to_remove self.install_rpath = install_rpath self.install_mode = install_mode self.optional = optional @@ -84,11 +115,12 @@ class ExecutableSerialisation: class TestSerialisation: def __init__(self, name: str, project: str, suite: str, fname: T.List[str], - is_cross_built: bool, exe_wrapper: T.Optional[build.Executable], + is_cross_built: bool, exe_wrapper: T.Optional[dependencies.ExternalProgram], needs_exe_wrapper: bool, is_parallel: bool, cmd_args: T.List[str], env: build.EnvironmentVariables, should_fail: bool, timeout: T.Optional[int], workdir: T.Optional[str], - extra_paths: T.List[str], protocol: str, priority: int): + extra_paths: T.List[str], protocol: TestProtocol, priority: int, + cmd_is_built: bool): self.name = name self.project_name = project self.suite = suite @@ -107,8 +139,10 @@ class TestSerialisation: self.protocol = protocol self.priority = priority self.needs_exe_wrapper = needs_exe_wrapper + self.cmd_is_built = cmd_is_built + -def get_backend_from_name(backend: str, build: T.Optional[build.Build] = None, interpreter: T.Optional[Interpreter] = None) -> T.Optional['Backend']: +def get_backend_from_name(backend: str, build: T.Optional[build.Build] = None, interpreter: T.Optional['Interpreter'] = None) -> T.Optional['Backend']: if backend == 'ninja': from . import ninjabackend return ninjabackend.NinjaBackend(build, interpreter) @@ -135,7 +169,7 @@ def get_backend_from_name(backend: str, build: T.Optional[build.Build] = None, i # This class contains the basic functionality that is needed by all backends. # Feel free to move stuff in and out of it as you see fit. class Backend: - def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional[Interpreter]): + def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional['Interpreter']): # Make it possible to construct a dummy backend # This is used for introspection without a build directory if build is None: @@ -150,9 +184,9 @@ class Backend: self.build_to_src = mesonlib.relpath(self.environment.get_source_dir(), self.environment.get_build_dir()) - def get_target_filename(self, t): + def get_target_filename(self, t, *, warn_multi_output: bool = True): if isinstance(t, build.CustomTarget): - if len(t.get_outputs()) != 1: + if warn_multi_output and len(t.get_outputs()) != 1: mlog.warning('custom_target {!r} has more than one output! ' 'Using the first one.'.format(t.name)) filename = t.get_outputs()[0] @@ -197,7 +231,7 @@ class Backend: return os.path.join(self.get_target_dir(target), target.get_filename()) elif isinstance(target, (build.CustomTarget, build.CustomTargetIndex)): if not target.is_linkable_target(): - raise MesonException('Tried to link against custom target "%s", which is not linkable.' % target.name) + raise MesonException('Tried to link against custom target "{}", which is not linkable.'.format(target.name)) return os.path.join(self.get_target_dir(target), target.get_filename()) elif isinstance(target, build.Executable): if target.import_filename: @@ -228,7 +262,7 @@ class Backend: return self.build_to_src def get_target_private_dir(self, target): - return os.path.join(self.get_target_dir(target), target.get_id()) + return os.path.join(self.get_target_filename(target, warn_multi_output=False) + '.p') def get_target_private_dir_abs(self, target): return os.path.join(self.environment.get_build_dir(), self.get_target_private_dir(target)) @@ -283,7 +317,7 @@ class Backend: ofile = init_language_file(comp.get_default_suffix(), unity_file_number) unity_file_number += 1 files_in_current = 0 - ofile.write('#include<%s>\n' % src) + ofile.write('#include<{}>\n'.format(src)) files_in_current += 1 if ofile: ofile.close() @@ -413,6 +447,46 @@ class Backend: return True return False + def get_external_rpath_dirs(self, target): + dirs = set() + args = [] + # FIXME: is there a better way? + for lang in ['c', 'cpp']: + try: + args.extend(self.environment.coredata.get_external_link_args(target.for_machine, lang)) + except Exception: + pass + # Match rpath formats: + # -Wl,-rpath= + # -Wl,-rpath, + rpath_regex = re.compile(r'-Wl,-rpath[=,]([^,]+)') + # Match solaris style compat runpath formats: + # -Wl,-R + # -Wl,-R, + runpath_regex = re.compile(r'-Wl,-R[,]?([^,]+)') + # Match symbols formats: + # -Wl,--just-symbols= + # -Wl,--just-symbols, + symbols_regex = re.compile(r'-Wl,--just-symbols[=,]([^,]+)') + for arg in args: + rpath_match = rpath_regex.match(arg) + if rpath_match: + for dir in rpath_match.group(1).split(':'): + dirs.add(dir) + runpath_match = runpath_regex.match(arg) + if runpath_match: + for dir in runpath_match.group(1).split(':'): + # The symbols arg is an rpath if the path is a directory + if Path(dir).is_dir(): + dirs.add(dir) + symbols_match = symbols_regex.match(arg) + if symbols_match: + for dir in symbols_match.group(1).split(':'): + # Prevent usage of --just-symbols to specify rpath + if Path(dir).is_dir(): + raise MesonException('Invalid arg for --just-symbols, {} is a directory.'.format(dir)) + return dirs + def rpaths_for_bundled_shared_libraries(self, target, exclude_system=True): paths = [] for dep in target.external_deps: @@ -427,6 +501,9 @@ class Backend: if exclude_system and self._libdir_is_system(libdir, target.compilers, self.environment): # No point in adding system paths. continue + # Don't remove rpaths specified in LDFLAGS. + if libdir in self.get_external_rpath_dirs(target): + continue # Windows doesn't support rpaths, but we use this function to # emulate rpaths by setting PATH, so also accept DLLs here if os.path.splitext(libpath)[1] not in ['.dll', '.lib', '.so', '.dylib']: @@ -446,8 +523,15 @@ class Backend: result = OrderedSet() result.add('meson-out') result.update(self.rpaths_for_bundled_shared_libraries(target)) + target.rpath_dirs_to_remove.update([d.encode('utf8') for d in result]) return tuple(result) + @staticmethod + def canonicalize_filename(fname): + for ch in ('/', '\\', ':'): + fname = fname.replace(ch, '_') + return fname + def object_filename_from_source(self, target, source): assert isinstance(source, mesonlib.File) build_dir = self.environment.get_build_dir() @@ -478,7 +562,7 @@ class Backend: source = os.path.relpath(os.path.join(build_dir, rel_src), os.path.join(self.environment.get_source_dir(), target.get_subdir())) machine = self.environment.machines[target.for_machine] - return source.replace('/', '_').replace('\\', '_') + '.' + machine.get_object_suffix() + return self.canonicalize_filename(source) + '.' + machine.get_object_suffix() def determine_ext_objs(self, extobj, proj_dir_to_build_root): result = [] @@ -538,14 +622,14 @@ class Backend: def create_msvc_pch_implementation(self, target, lang, pch_header): # We have to include the language in the file name, otherwise # pch.c and pch.cpp will both end up as pch.obj in VS backends. - impl_name = 'meson_pch-%s.%s' % (lang, lang) + impl_name = 'meson_pch-{}.{}'.format(lang, lang) pch_rel_to_build = os.path.join(self.get_target_private_dir(target), impl_name) # Make sure to prepend the build dir, since the working directory is # not defined. Otherwise, we might create the file in the wrong path. pch_file = os.path.join(self.build_dir, pch_rel_to_build) os.makedirs(os.path.dirname(pch_file), exist_ok=True) - content = '#include "%s"' % os.path.basename(pch_header) + content = '#include "{}"'.format(os.path.basename(pch_header)) pch_file_tmp = pch_file + '.tmp' with open(pch_file_tmp, 'w') as f: f.write(content) @@ -554,36 +638,20 @@ class Backend: @staticmethod def escape_extra_args(compiler, args): - # No extra escaping/quoting needed when not running on Windows - if not mesonlib.is_windows(): - return args + # all backslashes in defines are doubly-escaped extra_args = [] - # Compiler-specific escaping is needed for -D args but not for any others - if isinstance(compiler, VisualStudioLikeCompiler): - # MSVC needs escaping when a -D argument ends in \ or \" - for arg in args: - if arg.startswith('-D') or arg.startswith('/D'): - # Without extra escaping for these two, the next character - # gets eaten - if arg.endswith('\\'): - arg += '\\' - elif arg.endswith('\\"'): - arg = arg[:-2] + '\\\\"' - extra_args.append(arg) - else: - # MinGW GCC needs all backslashes in defines to be doubly-escaped - # FIXME: Not sure about Cygwin or Clang - for arg in args: - if arg.startswith('-D') or arg.startswith('/D'): - arg = arg.replace('\\', '\\\\') - extra_args.append(arg) + for arg in args: + if arg.startswith('-D') or arg.startswith('/D'): + arg = arg.replace('\\', '\\\\') + extra_args.append(arg) + return extra_args def generate_basic_compiler_args(self, target, compiler, no_warn_args=False): # Create an empty commands list, and start adding arguments from # various sources in the order in which they must override each other # starting from hard-coded defaults followed by build options and so on. - commands = CompilerArgs(compiler) + commands = compiler.compiler_args() copt_proxy = self.get_compiler_options_for_target(target)[compiler.language] # First, the trivial ones that are impossible to override. @@ -665,7 +733,7 @@ class Backend: args = [] for d in deps: if not (d.is_linkable_target()): - raise RuntimeError('Tried to link with a non-library target "%s".' % d.get_basename()) + raise RuntimeError('Tried to link with a non-library target "{}".'.format(d.get_basename())) arg = self.get_target_filename_for_linking(d) if not arg: continue @@ -706,6 +774,7 @@ class Backend: for deppath in self.rpaths_for_bundled_shared_libraries(target, exclude_system=False): result.add(os.path.normpath(os.path.join(self.environment.get_build_dir(), deppath))) for bdep in extra_bdeps: + prospectives.add(bdep) prospectives.update(bdep.get_transitive_link_deps()) # Internal deps for ld in prospectives: @@ -738,7 +807,16 @@ class Backend: # E.g. an external verifier or simulator program run on a generated executable. # Can always be run without a wrapper. test_for_machine = MachineChoice.BUILD - is_cross = not self.environment.machines.matches_build_machine(test_for_machine) + + # we allow passing compiled executables to tests, which may be cross built. + # We need to consider these as well when considering whether the target is cross or not. + for a in t.cmd_args: + if isinstance(a, build.BuildTarget): + if a.for_machine is MachineChoice.HOST: + test_for_machine = MachineChoice.HOST + break + + is_cross = self.environment.is_cross_build(test_for_machine) if is_cross and self.environment.need_exe_wrapper(): exe_wrapper = self.environment.get_exe_wrapper() else: @@ -751,6 +829,7 @@ class Backend: extra_paths = self.determine_windows_extra_paths(exe, extra_bdeps) else: extra_paths = [] + cmd_args = [] for a in unholder(t.cmd_args): if isinstance(a, build.BuildTarget): @@ -760,6 +839,11 @@ class Backend: cmd_args.append(a) elif isinstance(a, str): cmd_args.append(a) + elif isinstance(a, build.Executable): + p = self.construct_target_rel_path(a, t.workdir) + if p == a.get_filename(): + p = './' + p + cmd_args.append(p) elif isinstance(a, build.Target): cmd_args.append(self.construct_target_rel_path(a, t.workdir)) else: @@ -768,7 +852,8 @@ class Backend: exe_wrapper, self.environment.need_exe_wrapper(), t.is_parallel, cmd_args, t.env, t.should_fail, t.timeout, t.workdir, - extra_paths, t.protocol, t.priority) + extra_paths, t.protocol, t.priority, + isinstance(exe, build.Executable)) arr.append(ts) return arr @@ -854,7 +939,7 @@ class Backend: m = regex.search(arg) while m is not None: index = int(m.group(1)) - src = '@OUTPUT%d@' % index + src = '@OUTPUT{}@'.format(index) arg = arg.replace(src, os.path.join(private_dir, output_list[index])) m = regex.search(arg) newargs.append(arg) @@ -981,35 +1066,36 @@ class Backend: elif not isinstance(i, str): err_msg = 'Argument {0} is of unknown type {1}' raise RuntimeError(err_msg.format(str(i), str(type(i)))) - elif '@SOURCE_ROOT@' in i: - i = i.replace('@SOURCE_ROOT@', source_root) - elif '@BUILD_ROOT@' in i: - i = i.replace('@BUILD_ROOT@', build_root) - elif '@DEPFILE@' in i: - if target.depfile is None: - msg = 'Custom target {!r} has @DEPFILE@ but no depfile ' \ - 'keyword argument.'.format(target.name) - raise MesonException(msg) - dfilename = os.path.join(outdir, target.depfile) - i = i.replace('@DEPFILE@', dfilename) - elif '@PRIVATE_DIR@' in i: - if target.absolute_paths: - pdir = self.get_target_private_dir_abs(target) - else: - pdir = self.get_target_private_dir(target) - i = i.replace('@PRIVATE_DIR@', pdir) - elif '@PRIVATE_OUTDIR_' in i: - match = re.search(r'@PRIVATE_OUTDIR_(ABS_)?([^/\s*]*)@', i) - if not match: - msg = 'Custom target {!r} has an invalid argument {!r}' \ - ''.format(target.name, i) - raise MesonException(msg) - source = match.group(0) - if match.group(1) is None and not target.absolute_paths: - lead_dir = '' - else: - lead_dir = self.environment.get_build_dir() - i = i.replace(source, os.path.join(lead_dir, outdir)) + else: + if '@SOURCE_ROOT@' in i: + i = i.replace('@SOURCE_ROOT@', source_root) + if '@BUILD_ROOT@' in i: + i = i.replace('@BUILD_ROOT@', build_root) + if '@DEPFILE@' in i: + if target.depfile is None: + msg = 'Custom target {!r} has @DEPFILE@ but no depfile ' \ + 'keyword argument.'.format(target.name) + raise MesonException(msg) + dfilename = os.path.join(outdir, target.depfile) + i = i.replace('@DEPFILE@', dfilename) + if '@PRIVATE_DIR@' in i: + if target.absolute_paths: + pdir = self.get_target_private_dir_abs(target) + else: + pdir = self.get_target_private_dir(target) + i = i.replace('@PRIVATE_DIR@', pdir) + if '@PRIVATE_OUTDIR_' in i: + match = re.search(r'@PRIVATE_OUTDIR_(ABS_)?([^/\s*]*)@', i) + if not match: + msg = 'Custom target {!r} has an invalid argument {!r}' \ + ''.format(target.name, i) + raise MesonException(msg) + source = match.group(0) + if match.group(1) is None and not target.absolute_paths: + lead_dir = '' + else: + lead_dir = self.environment.get_build_dir() + i = i.replace(source, os.path.join(lead_dir, outdir)) cmd.append(i) # Substitute the rest of the template strings values = mesonlib.get_filenames_templates_dict(inputs, outputs) @@ -1110,6 +1196,7 @@ class Backend: mappings = t.get_link_deps_mapping(d.prefix, self.environment) i = TargetInstallData(self.get_target_filename(t), outdirs[0], t.get_aliases(), should_strip, mappings, + t.rpath_dirs_to_remove, t.install_rpath, install_mode) d.targets.append(i) @@ -1127,14 +1214,14 @@ class Backend: implib_install_dir = self.environment.get_import_lib_dir() # Install the import library; may not exist for shared modules i = TargetInstallData(self.get_target_filename_for_linking(t), - implib_install_dir, {}, False, {}, '', install_mode, + implib_install_dir, {}, False, {}, set(), '', install_mode, optional=isinstance(t, build.SharedModule)) d.targets.append(i) if not should_strip and t.get_debug_filename(): debug_file = os.path.join(self.get_target_dir(t), t.get_debug_filename()) i = TargetInstallData(debug_file, outdirs[0], - {}, False, {}, '', + {}, False, {}, set(), '', install_mode, optional=True) d.targets.append(i) # Install secondary outputs. Only used for Vala right now. @@ -1144,7 +1231,7 @@ class Backend: if outdir is False: continue f = os.path.join(self.get_target_dir(t), output) - i = TargetInstallData(f, outdir, {}, False, {}, None, install_mode) + i = TargetInstallData(f, outdir, {}, False, {}, set(), None, install_mode) d.targets.append(i) elif isinstance(t, build.CustomTarget): # If only one install_dir is specified, assume that all @@ -1157,7 +1244,7 @@ class Backend: if num_outdirs == 1 and num_out > 1: for output in t.get_outputs(): f = os.path.join(self.get_target_dir(t), output) - i = TargetInstallData(f, outdirs[0], {}, False, {}, None, install_mode, + i = TargetInstallData(f, outdirs[0], {}, False, {}, set(), None, install_mode, optional=not t.build_by_default) d.targets.append(i) else: @@ -1166,7 +1253,7 @@ class Backend: if outdir is False: continue f = os.path.join(self.get_target_dir(t), output) - i = TargetInstallData(f, outdir, {}, False, {}, None, install_mode, + i = TargetInstallData(f, outdir, {}, False, {}, set(), None, install_mode, optional=not t.build_by_default) d.targets.append(i) |