Merge pull request #7231 from mensinda/cmOverride

cmake: Add more advanced subproject configuration options
pull/7405/head
Jussi Pakkanen 4 years ago committed by GitHub
commit 64f36613ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 77
      docs/markdown/CMake-module.md
  2. 17
      docs/markdown/snippets/cmake.md
  3. 5
      mesonbuild/cmake/__init__.py
  4. 95
      mesonbuild/cmake/common.py
  5. 21
      mesonbuild/cmake/interpreter.py
  6. 12
      mesonbuild/interpreter.py
  7. 110
      mesonbuild/modules/cmake.py
  8. 18
      test cases/cmake/19 advanced options/main.cpp
  9. 29
      test cases/cmake/19 advanced options/meson.build
  10. 18
      test cases/cmake/19 advanced options/subprojects/cmOpts/CMakeLists.txt
  11. 31
      test cases/cmake/19 advanced options/subprojects/cmOpts/cmMod.cpp
  12. 14
      test cases/cmake/19 advanced options/subprojects/cmOpts/cmMod.hpp
  13. 25
      test cases/cmake/19 advanced options/subprojects/cmOpts/cmTest.cpp
  14. 3
      test cases/cmake/19 advanced options/subprojects/cmOpts/cmTest.hpp
  15. 10
      test cases/cmake/19 advanced options/subprojects/cmOpts/main.cpp
  16. 8
      test cases/cmake/19 advanced options/test.json

@ -48,8 +48,6 @@ The `subproject` method is almost identical to the normal meson
`subproject` function. The only difference is that a CMake project
instead of a meson project is configured.
Also, project specific CMake options can be added with the `cmake_options` key.
The returned `sub_proj` supports the same options as a "normal" subproject.
Meson automatically detects CMake build targets, which can be accessed with
the methods listed [below](#subproject-object).
@ -87,6 +85,49 @@ It should be noted that not all projects are guaranteed to work. The
safest approach would still be to create a `meson.build` for the
subprojects in question.
### Configuration options
*New in meson 0.55.0*
Meson also supports passing configuration options to CMake and overriding
certain build details extracted from the CMake subproject.
```meson
cmake = import('cmake')
opt_var = cmake.subproject_options()
# Call CMake with `-DSOME_OTHER_VAR=ON`
opt_var.add_cmake_defines({'SOME_OTHER_VAR': true})
# Globally override the C++ standard to c++11
opt_var.set_override_option('cpp_std', 'c++11')
# Override the previous global C++ standard
# with c++14 only for the CMake target someLib
opt_var.set_override_option('cpp_std', 'c++14', target: 'someLib')
sub_pro = cmake.subproject('someLibProject', options: opt_var)
# Further changes to opt_var have no effect
```
See [the CMake options object](#cmake-options-object) for a complete reference
of all supported functions.
The CMake configuration options object is very similar to the
[configuration data object](Reference-manual.md#configuration-data-object) object
returned by [`configuration_data`](Reference-manual.md#configuration_data). It
is generated by the `subproject_options` function
All configuration options have to be set *before* the subproject is configured
and must be passed to the `subproject` method via the `options` key. Altering
the configuration object won't have any effect on previous `cmake.subproject`
calls.
In earlier meson versions CMake command-line parameters could be set with the
`cmake_options` kwarg. However, this feature is deprecated since 0.55.0 and only
kept for compatibility. It will not work together with the `options` kwarg.
### `subproject` object
This object is returned by the `subproject` function described above
@ -103,7 +144,37 @@ and supports the following methods:
the subproject. Usually `dependency()` or `target()` should be
preferred to extract build targets.
- `found` returns true if the subproject is available, otherwise false
*new in in 0.53.2*
*new in meson 0.53.2*
### `cmake options` object
This object is returned by the `subproject_options()` function and consumed by
the `options` kwarg of the `subproject` function. The following methods are
supported:
- `add_cmake_defines({'opt1': val1, ...})` add additional CMake commandline defines
- `set_override_option(opt, val)` set specific [build options](Build-options.md)
for targets. This will effectively add `opt=val` to the `override_options`
array of the [build target](Reference-manual.md#executable)
- `set_install(bool)` override wether targets should be installed or not
- `append_compile_args(lang, arg1, ...)` append compile flags for a specific
language to the targets
- `append_link_args(arg1, ...)` append linger args to the targets
- `clear()` reset all data in the `cmake options` object
The methods `set_override_option`, `set_install`, `append_compile_args` and
`append_link_args` support the optional `target` kwarg. If specified, the set
options affect the specific target. The effect of the option is global for the
subproject otherwise.
If, for instance, `opt_var.set_install(false)` is called, no target will be
installed regardless of what is set by CMake. However, it is still possible to
install specific targets (here `foo`) by setting the `target` kwarg:
`opt_var.set_install(true, target: 'foo')`
Options that are not set won't affect the generated subproject. So, if for
instance, `set_install` was not called then the values extracted from CMake will
be used.
## CMake configuration files

@ -0,0 +1,17 @@
## Configure CMake subprojects with meson.subproject_options
Meson now supports passing configuration options to CMake and overriding
certain build details extracted from the CMake subproject.
The new CMake configuration options object is very similar to the
[configuration data object](Reference-manual.md#configuration-data-object) object
returned by [`configuration_data`](Reference-manual.md#configuration_data). It
is generated by the `subproject_options` function
All configuration options have to be set *before* the subproject is configured
and must be passed to the `subproject` method via the `options` key. Altering
the configuration object won't have any effect on previous `cmake.subproject`
calls.
**Note:** The `cmake_options` kwarg for the `subproject` function is now
deprecated since it is replaced by the new `options` system.

@ -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

@ -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

@ -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
@ -993,7 +993,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')
@ -1154,21 +1154,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':

@ -2444,7 +2444,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)
@ -2867,13 +2867,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():

@ -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
@ -286,16 +367,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)

@ -0,0 +1,18 @@
#include <iostream>
#include <cmMod.hpp>
#include <cmTest.hpp>
using namespace std;
int main(void) {
cmModClass obj("Hello");
cout << obj.getStr() << endl;
int v1 = obj.getInt();
int v2 = getTestInt();
if (v1 != ((1 + v2) * 2)) {
cerr << "Number test failed" << endl;
return 1;
}
return 0;
}

@ -0,0 +1,29 @@
project('cmake_set_opt', ['c', 'cpp'])
comp = meson.get_compiler('cpp')
if comp.get_argument_syntax() == 'msvc'
error('MESON_SKIP_TEST: MSVC is not supported because it does not support C++11')
endif
cm = import('cmake')
opts = cm.subproject_options()
opts.add_cmake_defines({'SOME_CMAKE_VAR': 'something', 'SOME_OTHER_VAR': true})
opts.set_override_option('cpp_std', 'c++11') # Global is C++11
opts.set_override_option('cpp_std', 'c++14', target: 'cmModLib++') # Override it with C++14 for cmModLib++
opts.append_compile_args('cpp', '-DMESON_GLOBAL_FLAG=1')
opts.append_compile_args('cpp', ['-DMESON_SPECIAL_FLAG1=1', ['-DMESON_SPECIAL_FLAG2=1']], target: 'cmModLib++')
opts.append_compile_args('cpp', '-DMESON_MAGIC_INT=42', target: 'cmModLib++')
opts.append_compile_args('cpp', [[[['-DMESON_MAGIC_INT=20']]]], target: 'cmTestLib')
opts.set_install(false)
opts.set_install(true, target: 'testEXE')
sp = cm.subproject('cmOpts', options: opts)
dep1 = sp.dependency('cmModLib++')
dep2 = sp.dependency('cmTestLib')
exe1 = executable('main', ['main.cpp'], dependencies: [dep1, dep2])
test('test1', exe1)

@ -0,0 +1,18 @@
cmake_minimum_required(VERSION 3.7)
project(CmOpts)
set(CMAKE_CXX_STANDARD 98)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT "${SOME_CMAKE_VAR}" STREQUAL "something")
message(FATAL_ERROR "Setting the CMake var failed")
endif()
add_library(cmModLib++ STATIC cmMod.cpp)
add_library(cmTestLib STATIC cmTest.cpp)
add_executable(testEXE main.cpp)
target_link_libraries(testEXE cmModLib++)
install(TARGETS cmTestLib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin)

@ -0,0 +1,31 @@
#include "cmMod.hpp"
using namespace std;
#if __cplusplus < 201402L
#error "At least C++14 is required"
#endif
#ifndef MESON_GLOBAL_FLAG
#error "MESON_GLOBAL_FLAG was not set"
#endif
#ifndef MESON_SPECIAL_FLAG1
#error "MESON_SPECIAL_FLAG1 was not set"
#endif
#ifndef MESON_SPECIAL_FLAG2
#error "MESON_SPECIAL_FLAG2 was not set"
#endif
cmModClass::cmModClass(string foo) {
str = foo + " World";
}
string cmModClass::getStr() const {
return str;
}
int cmModClass::getInt() const {
return MESON_MAGIC_INT;
}

@ -0,0 +1,14 @@
#pragma once
#include <string>
class cmModClass {
private:
std::string str;
public:
cmModClass(std::string foo);
std::string getStr() const;
int getInt() const;
};

@ -0,0 +1,25 @@
#include "cmTest.hpp"
#if __cplusplus < 201103L
#error "At least C++11 is required"
#endif
#if __cplusplus >= 201402L
#error "At most C++11 is required"
#endif
#ifndef MESON_GLOBAL_FLAG
#error "MESON_GLOBAL_FLAG was not set"
#endif
#ifdef MESON_SPECIAL_FLAG1
#error "MESON_SPECIAL_FLAG1 *was* set"
#endif
#ifdef MESON_SPECIAL_FLAG2
#error "MESON_SPECIAL_FLAG2 *was* set"
#endif
int getTestInt() {
return MESON_MAGIC_INT;
}

@ -0,0 +1,10 @@
#include <iostream>
#include "cmMod.hpp"
using namespace std;
int main(void) {
cmModClass obj("Hello (LIB TEST)");
cout << obj.getStr() << endl;
return 0;
}

@ -0,0 +1,8 @@
{
"installed": [
{"type": "exe", "file": "usr/bin/cm_testEXE"}
],
"tools": {
"cmake": ">=3.11"
}
}
Loading…
Cancel
Save