diff --git a/mesonbuild/cmake/__init__.py b/mesonbuild/cmake/__init__.py index 01cc3f90b..db7aefd6f 100644 --- a/mesonbuild/cmake/__init__.py +++ b/mesonbuild/cmake/__init__.py @@ -24,11 +24,14 @@ __all__ = [ 'CMakeTarget', 'CMakeTraceLine', 'CMakeTraceParser', + 'SingleTargetOptions', + 'TargetOptions', 'parse_generator_expressions', 'language_map', + 'cmake_defines_to_args', ] -from .common import CMakeException +from .common import CMakeException, SingleTargetOptions, TargetOptions, cmake_defines_to_args from .client import CMakeClient from .executor import CMakeExecutor from .fileapi import CMakeFileAPI diff --git a/mesonbuild/cmake/common.py b/mesonbuild/cmake/common.py index e7da0d798..4510b5d5a 100644 --- a/mesonbuild/cmake/common.py +++ b/mesonbuild/cmake/common.py @@ -60,6 +60,26 @@ def _flags_to_list(raw: str) -> T.List[str]: res = list(filter(lambda x: len(x) > 0, res)) return res +def cmake_defines_to_args(raw: T.Any, permissive: bool = False) -> T.List[str]: + res = [] # type: T.List[str] + if not isinstance(raw, list): + raw = [raw] + + for i in raw: + if not isinstance(i, dict): + raise MesonException('Invalid CMake defines. Expected a dict, but got a {}'.format(type(i).__name__)) + for key, val in i.items(): + assert isinstance(key, str) + if isinstance(val, (str, int, float)): + res += ['-D{}={}'.format(key, val)] + elif isinstance(val, bool): + val_str = 'ON' if val else 'OFF' + res += ['-D{}={}'.format(key, val_str)] + else: + raise MesonException('Type "{}" of "{}" is not supported as for a CMake define value'.format(type(val).__name__, key)) + + return res + class CMakeFileGroup: def __init__(self, data: dict): self.defines = data.get('defines', '') @@ -163,3 +183,78 @@ class CMakeConfiguration: mlog.log('Project {}:'.format(idx)) with mlog.nested(): i.log() + +class SingleTargetOptions: + def __init__(self) -> None: + self.opts = {} # type: T.Dict[str, str] + self.lang_args = {} # type: T.Dict[str, T.List[str]] + self.link_args = [] # type: T.List[str] + self.install = 'preserve' + + def set_opt(self, opt: str, val: str) -> None: + self.opts[opt] = val + + def append_args(self, lang: str, args: T.List[str]) -> None: + if lang not in self.lang_args: + self.lang_args[lang] = [] + self.lang_args[lang] += args + + def append_link_args(self, args: T.List[str]) -> None: + self.link_args += args + + def set_install(self, install: bool) -> None: + self.install = 'true' if install else 'false' + + def get_override_options(self, initial: T.List[str]) -> T.List[str]: + res = [] # type: T.List[str] + for i in initial: + opt = i[:i.find('=')] + if opt not in self.opts: + res += [i] + res += ['{}={}'.format(k, v) for k, v in self.opts.items()] + return res + + def get_compile_args(self, lang: str, initial: T.List[str]) -> T.List[str]: + if lang in self.lang_args: + return initial + self.lang_args[lang] + return initial + + def get_link_args(self, initial: T.List[str]) -> T.List[str]: + return initial + self.link_args + + def get_install(self, initial: bool) -> bool: + return {'preserve': initial, 'true': True, 'false': False}[self.install] + +class TargetOptions: + def __init__(self) -> None: + self.global_options = SingleTargetOptions() + self.target_options = {} # type: T.Dict[str, SingleTargetOptions] + + def __getitem__(self, tgt: str) -> SingleTargetOptions: + if tgt not in self.target_options: + self.target_options[tgt] = SingleTargetOptions() + return self.target_options[tgt] + + def get_override_options(self, tgt: str, initial: T.List[str]) -> T.List[str]: + initial = self.global_options.get_override_options(initial) + if tgt in self.target_options: + initial = self.target_options[tgt].get_override_options(initial) + return initial + + def get_compile_args(self, tgt: str, lang: str, initial: T.List[str]) -> T.List[str]: + initial = self.global_options.get_compile_args(lang, initial) + if tgt in self.target_options: + initial = self.target_options[tgt].get_compile_args(lang, initial) + return initial + + def get_link_args(self, tgt: str, initial: T.List[str]) -> T.List[str]: + initial = self.global_options.get_link_args(initial) + if tgt in self.target_options: + initial = self.target_options[tgt].get_link_args(initial) + return initial + + def get_install(self, tgt: str, initial: bool) -> bool: + initial = self.global_options.get_install(initial) + if tgt in self.target_options: + initial = self.target_options[tgt].get_install(initial) + return initial diff --git a/mesonbuild/cmake/interpreter.py b/mesonbuild/cmake/interpreter.py index a5bf545d5..57e6e1d63 100644 --- a/mesonbuild/cmake/interpreter.py +++ b/mesonbuild/cmake/interpreter.py @@ -17,7 +17,7 @@ import pkg_resources -from .common import CMakeException, CMakeTarget +from .common import CMakeException, CMakeTarget, TargetOptions from .client import CMakeClient, RequestCMakeInputs, RequestConfigure, RequestCompute, RequestCodeModel from .fileapi import CMakeFileAPI from .executor import CMakeExecutor @@ -994,7 +994,7 @@ class CMakeInterpreter: mlog.log('CMake project', mlog.bold(self.project_name), 'has', mlog.bold(str(len(self.targets) + len(self.custom_targets))), 'build targets.') - def pretend_to_be_meson(self) -> CodeBlockNode: + def pretend_to_be_meson(self, options: TargetOptions) -> CodeBlockNode: if not self.project_name: raise CMakeException('CMakeInterpreter was not analysed') @@ -1158,21 +1158,26 @@ class CMakeInterpreter: dep_var = '{}_dep'.format(tgt.name) tgt_var = tgt.name + install_tgt = options.get_install(tgt.cmake_name, tgt.install) + # Generate target kwargs tgt_kwargs = { - 'build_by_default': tgt.install, - 'link_args': tgt.link_flags + tgt.link_libraries, + 'build_by_default': install_tgt, + 'link_args': options.get_link_args(tgt.cmake_name, tgt.link_flags + tgt.link_libraries), 'link_with': link_with, 'include_directories': id_node(inc_var), - 'install': tgt.install, - 'install_dir': tgt.install_dir, - 'override_options': tgt.override_options, + 'install': install_tgt, + 'override_options': options.get_override_options(tgt.cmake_name, tgt.override_options), 'objects': [method(x, 'extract_all_objects') for x in objec_libs], } + # Only set if installed and only override if it is set + if install_tgt and tgt.install_dir: + tgt_kwargs['install_dir'] = tgt.install_dir + # Handle compiler args for key, val in tgt.compile_opts.items(): - tgt_kwargs['{}_args'.format(key)] = val + tgt_kwargs['{}_args'.format(key)] = options.get_compile_args(tgt.cmake_name, key, val) # Handle -fPCI, etc if tgt_func == 'executable': diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index b8d4fec75..740b0bcea 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -2440,7 +2440,7 @@ class Interpreter(InterpreterBase): if isinstance(item, build.CustomTarget): return CustomTargetHolder(item, self) - elif isinstance(item, (int, str, bool, Disabler)) or item is None: + elif isinstance(item, (int, str, bool, Disabler, InterpreterObject)) or item is None: return item elif isinstance(item, build.Executable): return ExecutableHolder(item, self) @@ -2851,13 +2851,21 @@ external dependencies (including libraries) must go to "dependencies".''') with mlog.nested(): new_build = self.build.copy() prefix = self.coredata.builtins['prefix'].value + + from .modules.cmake import CMakeSubprojectOptions + options = kwargs.get('options', CMakeSubprojectOptions()) + if not isinstance(options, CMakeSubprojectOptions): + raise InterpreterException('"options" kwarg must be CMakeSubprojectOptions' + ' object (created by cmake.subproject_options())') + cmake_options = mesonlib.stringlistify(kwargs.get('cmake_options', [])) + cmake_options += options.cmake_options cm_int = CMakeInterpreter(new_build, subdir, subdir_abs, prefix, new_build.environment, self.backend) cm_int.initialise(cmake_options) cm_int.analyse() # Generate a meson ast and execute it with the normal do_subproject_meson - ast = cm_int.pretend_to_be_meson() + ast = cm_int.pretend_to_be_meson(options.target_options) mlog.log() with mlog.nested(): diff --git a/mesonbuild/modules/cmake.py b/mesonbuild/modules/cmake.py index 6c4098baa..dcbeda864 100644 --- a/mesonbuild/modules/cmake.py +++ b/mesonbuild/modules/cmake.py @@ -14,12 +14,28 @@ import re import os, os.path, pathlib import shutil +import typing as T from . import ExtensionModule, ModuleReturnValue from .. import build, dependencies, mesonlib, mlog -from ..interpreterbase import permittedKwargs, FeatureNew, stringArgs, InterpreterObject, ObjectHolder, noPosargs +from ..cmake import SingleTargetOptions, TargetOptions, cmake_defines_to_args from ..interpreter import ConfigurationDataHolder, InterpreterException, SubprojectHolder +from ..interpreterbase import ( + InterpreterObject, + ObjectHolder, + + FeatureNew, + FeatureNewKwargs, + FeatureDeprecatedKwargs, + + stringArgs, + permittedKwargs, + noPosargs, + noKwargs, + + InvalidArguments, +) COMPATIBILITIES = ['AnyNewerVersion', 'SameMajorVersion', 'SameMinorVersion', 'ExactVersion'] @@ -82,42 +98,107 @@ class CMakeSubprojectHolder(InterpreterObject, ObjectHolder): assert(all([x in res for x in ['inc', 'src', 'dep', 'tgt', 'func']])) return res - @permittedKwargs({}) + @noKwargs + @stringArgs def get_variable(self, args, kwargs): return self.held_object.get_variable_method(args, kwargs) - @permittedKwargs({}) + @noKwargs + @stringArgs def dependency(self, args, kwargs): info = self._args_to_info(args) return self.get_variable([info['dep']], kwargs) - @permittedKwargs({}) + @noKwargs + @stringArgs def include_directories(self, args, kwargs): info = self._args_to_info(args) return self.get_variable([info['inc']], kwargs) - @permittedKwargs({}) + @noKwargs + @stringArgs def target(self, args, kwargs): info = self._args_to_info(args) return self.get_variable([info['tgt']], kwargs) - @permittedKwargs({}) + @noKwargs + @stringArgs def target_type(self, args, kwargs): info = self._args_to_info(args) return info['func'] @noPosargs - @permittedKwargs({}) + @noKwargs def target_list(self, args, kwargs): return self.held_object.cm_interpreter.target_list() @noPosargs - @permittedKwargs({}) + @noKwargs @FeatureNew('CMakeSubproject.found()', '0.53.2') def found_method(self, args, kwargs): return self.held_object is not None +class CMakeSubprojectOptions(InterpreterObject): + def __init__(self) -> None: + super().__init__() + self.cmake_options = [] # type: T.List[str] + self.target_options = TargetOptions() + + self.methods.update( + { + 'add_cmake_defines': self.add_cmake_defines, + 'set_override_option': self.set_override_option, + 'set_install': self.set_install, + 'append_compile_args': self.append_compile_args, + 'append_link_args': self.append_link_args, + 'clear': self.clear, + } + ) + + def _get_opts(self, kwargs: dict) -> SingleTargetOptions: + if 'target' in kwargs: + return self.target_options[kwargs['target']] + return self.target_options.global_options + + @noKwargs + def add_cmake_defines(self, args, kwargs) -> None: + self.cmake_options += cmake_defines_to_args(args) + + @stringArgs + @permittedKwargs({'target'}) + def set_override_option(self, args, kwargs) -> None: + if len(args) != 2: + raise InvalidArguments('set_override_option takes exactly 2 positional arguments') + self._get_opts(kwargs).set_opt(args[0], args[1]) + + @permittedKwargs({'target'}) + def set_install(self, args, kwargs) -> None: + if len(args) != 1 or not isinstance(args[0], bool): + raise InvalidArguments('set_install takes exactly 1 boolean argument') + self._get_opts(kwargs).set_install(args[0]) + + @stringArgs + @permittedKwargs({'target'}) + def append_compile_args(self, args, kwargs) -> None: + if len(args) < 2: + raise InvalidArguments('append_compile_args takes at least 2 positional arguments') + self._get_opts(kwargs).append_args(args[0], args[1:]) + + @stringArgs + @permittedKwargs({'target'}) + def append_link_args(self, args, kwargs) -> None: + if not args: + raise InvalidArguments('append_link_args takes at least 1 positional argument') + self._get_opts(kwargs).append_link_args(args) + + @noPosargs + @noKwargs + def clear(self, args, kwargs) -> None: + self.cmake_options.clear() + self.target_options = TargetOptions() + + class CmakeModule(ExtensionModule): cmake_detected = False cmake_root = None @@ -287,16 +368,27 @@ class CmakeModule(ExtensionModule): return res @FeatureNew('subproject', '0.51.0') - @permittedKwargs({'cmake_options', 'required'}) + @FeatureNewKwargs('subproject', '0.55.0', ['options']) + @FeatureDeprecatedKwargs('subproject', '0.55.0', ['cmake_options']) + @permittedKwargs({'cmake_options', 'required', 'options'}) @stringArgs def subproject(self, interpreter, state, args, kwargs): if len(args) != 1: raise InterpreterException('Subproject takes exactly one argument') + if 'cmake_options' in kwargs and 'options' in kwargs: + raise InterpreterException('"options" cannot be used together with "cmake_options"') dirname = args[0] subp = interpreter.do_subproject(dirname, 'cmake', kwargs) if not subp.held_object: return subp return CMakeSubprojectHolder(subp, dirname) + @FeatureNew('subproject_options', '0.55.0') + @noKwargs + @noPosargs + def subproject_options(self, state, args, kwargs) -> ModuleReturnValue: + opts = CMakeSubprojectOptions() + return ModuleReturnValue(opts, []) + def initialize(*args, **kwargs): return CmakeModule(*args, **kwargs)