diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3d04ffc9f..5ec62f105 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -88,6 +88,7 @@ jobs: gccx64ninja: {} variables: CYGWIN_ROOT: $(System.Workfolder)\cygwin + CYGWIN_CMAKE_LINK: http://cygwin.mirror.constant.com/x86_64/release/cmake/cmake-3.13.1-1.tar.xz CYGWIN_MIRROR: http://cygwin.mirror.constant.com steps: - script: | @@ -95,20 +96,28 @@ jobs: displayName: Install Cygwin - script: | %CYGWIN_ROOT%\cygwinsetup.exe -qnNdO -R "%CYGWIN_ROOT%" -s "%CYGWIN_MIRROR%" -g -P ^ - cmake,^ gcc-fortran,^ gcc-objc++,^ gcc-objc,^ git,^ gobject-introspection,^ + libarchive13,^ libboost-devel,^ libglib2.0-devel,^ libgtk3-devel,^ + libjsoncpp19,^ + librhash0,^ + libuv1,^ ninja,^ python35-pip,^ vala,^ + wget,^ zlib-devel displayName: Install Dependencies + - script: | + %CYGWIN_ROOT%\bin\bash.exe -cl "wget %CYGWIN_CMAKE_LINK% -O cmake.tar.xz" + %CYGWIN_ROOT%\bin\bash.exe -cl "tar -xf cmake.tar.xz -C /" + displayName: Manually install CMake 3.13.1 - script: | set BOOST_ROOT= set PATH=%CYGWIN_ROOT%\bin;%SYSTEMROOT%\system32 diff --git a/ci/azure-steps.yml b/ci/azure-steps.yml index abbed655e..77f61fe21 100644 --- a/ci/azure-steps.yml +++ b/ci/azure-steps.yml @@ -154,6 +154,11 @@ steps: where.exe python python --version + echo "" + echo "Locating cl, rc:" + where.exe cl + where.exe rc + echo "" echo "=== Start running tests ===" # Starting from VS2019 Powershell(?) will fail the test run diff --git a/docs/markdown/CMake-module.md b/docs/markdown/CMake-module.md index 4cc97cf1f..94f470821 100644 --- a/docs/markdown/CMake-module.md +++ b/docs/markdown/CMake-module.md @@ -1,6 +1,8 @@ # CMake module This module provides helper tools for generating cmake package files. +It also supports the usage of CMake based subprojects, similar to +the normal [meson subprojects](Subprojects.md). ## Usage @@ -10,6 +12,91 @@ following functions will then be available as methods on the object with the name `cmake`. You can, of course, replace the name `cmake` with anything else. +## CMake subprojects + +Using CMake subprojects is similar to using the "normal" meson +subprojects. They also have to be located in the `subprojects` +directory. + +Example: + +```cmake +add_library(cm_lib SHARED ${SOURCES}) +``` + +```meson +cmake = import('cmake') + +# Configure the CMake project +sub_proj = cmake.subproject('libsimple_cmake') + +# Fetch the dependency object +cm_lib = sub_proj.dependency('cm_lib') + +executable(exe1, ['sources'], dependencies: [cm_lib]) +``` + +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). + +It is usually enough to just use the dependency object returned by the +`dependency()` method in the build targets. This is almost identical to +using `declare_dependency()` object from a normal meson subproject. + +It is also possible to use executables defined in the CMake project as code +generators with the `target()` method: + +```cmake +add_executable(cm_exe ${EXE_SRC}) +``` + +```meson +cmake = import('cmake') + +# Subproject with the "code generator" +sub_pro = cmake.subproject('cmCodeGen') + +# Fetch the code generator exe +sub_exe = sub_pro.target('cm_exe') + +# Use the code generator +generated = custom_target( + 'cmake-generated', + input: [], + output: ['test.cpp'], + command: [sub_exe, '@OUTPUT@'] +) +``` + +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. + +### `subproject` object + +This object is returned by the `subproject` function described above +and supports the following methods: + + - `dependency(target)` returns a dependency object for any CMake target. + - `include_directories(target)` returns a meson `include_directories()` + object for the specified target. Using this function is not neccessary + if the dependency object is used. + - `target(target)` returns the raw build target. + - `target_type(target)` returns the type of the target as a string + - `target_list()` returns a list of all target *names*. + - `get_variable(name)` fetches the specified variable from inside + the subproject. Usually `dependency()` or `target()` should be + prefered to extract build targets. + +## CMake configuration files + ### cmake.write_basic_package_version_file() This function is the equivalent of the corresponding [CMake function](https://cmake.org/cmake/help/v3.11/module/CMakePackageConfigHelpers.html#generating-a-package-version-file), diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md index f59d6277f..0aa42537e 100644 --- a/docs/markdown/Reference-manual.md +++ b/docs/markdown/Reference-manual.md @@ -1082,7 +1082,7 @@ res2 = foo / bar ``` Builds a library that is either static, shared or both depending on -the value of `default_library` +the value of `default_library` user [option](https://mesonbuild.com/Builtin-options.html). You should use this instead of [`shared_library`](#shared_library), [`static_library`](#static_library) or @@ -2175,7 +2175,7 @@ an external dependency with the following methods: dep3 will add `['-Werror=foo', '-Werror=bar']` to the compiler args of any target it is added to, but libfoo will not be added to the link_args. - + *Note*: A bug present until 0.50.1 results in the above behavior not working correctly. diff --git a/docs/markdown/Subprojects.md b/docs/markdown/Subprojects.md index 254644185..fc845ff06 100644 --- a/docs/markdown/Subprojects.md +++ b/docs/markdown/Subprojects.md @@ -14,17 +14,19 @@ Meson tries to solve this problem by making it extremely easy to provide both at the same time. The way this is done is that Meson allows you to take any other Meson project and make it a part of your build without (in the best case) any changes to its Meson setup. It -becomes a transparent part of the project. +becomes a transparent part of the project. -It should be noted that this only works for subprojects that are built -with Meson. It can not be used with any other build system. The reason -is the simple fact that there is no possible way to do this reliably -with mixed build systems. +It should be noted that this is only guaranteed to work for subprojects +that are built with Meson. The reason is the simple fact that there is +no possible way to do this reliably with mixed build systems. Because of +this, only meson subprojects are described here. +[CMake based subprojects](CMake-module.md#CMake-subprojects) are also +supported but not guaranteed to work. ## A subproject example -Usually dependencies consist of some header files plus a library to link against. -To declare this internal dependency use `declare_dependency` function. +Usually dependencies consist of some header files plus a library to link against. +To declare this internal dependency use `declare_dependency` function. As an example, suppose we have a simple project that provides a shared library. Its `meson.build` would look like this. @@ -33,22 +35,22 @@ library. Its `meson.build` would look like this. project('libsimple', 'c') inc = include_directories('include') -libsimple = shared_library('simple', - 'simple.c', - include_directories : inc, +libsimple = shared_library('simple', + 'simple.c', + include_directories : inc, install : true) -libsimple_dep = declare_dependency(include_directories : inc, +libsimple_dep = declare_dependency(include_directories : inc, link_with : libsimple) ``` ### Naming convention for dependency variables -Ideally the dependency variable name should be of `_dep` form. +Ideally the dependency variable name should be of `_dep` form. This way one can just use it without even looking inside build definitions of that subproject. -In cases where there are multiple dependencies need to be declared, the default one -should be named as `_dep` (e.g. `gtest_dep`), and others can have +In cases where there are multiple dependencies need to be declared, the default one +should be named as `_dep` (e.g. `gtest_dep`), and others can have `___dep` form (e.g. `gtest_main_dep` - gtest with main function). There may be exceptions to these rules where common sense should be applied. @@ -65,16 +67,16 @@ as a subproject, use the `is_subproject` function. ## Using a subproject -All subprojects must be inside `subprojects` directory. -The `subprojects` directory must be at the top level of your project. -Subproject declaration must be in your top level `meson.build`. +All subprojects must be inside `subprojects` directory. +The `subprojects` directory must be at the top level of your project. +Subproject declaration must be in your top level `meson.build`. ### A simple example Let's use `libsimple` as a subproject. -At the top level of your project create `subprojects` directory. -Then copy `libsimple` into `subprojects` directory. +At the top level of your project create `subprojects` directory. +Then copy `libsimple` into `subprojects` directory. Your project's `meson.build` should look like this. @@ -84,9 +86,9 @@ project('my_project', 'cpp') libsimple_proj = subproject('libsimple') libsimple_dep = libsimple_proj.get_variable('libsimple_dep') -executable('my_project', - 'my_project.cpp', - dependencies : libsimple_dep, +executable('my_project', + 'my_project.cpp', + dependencies : libsimple_dep, install : true) ``` @@ -102,7 +104,7 @@ embed any sources. Some distros have a rule forbidding embedded dependencies so your project must be buildable without them or otherwise the packager will hate you. -Here's how you would use system libraries and fall back to embedding sources +Here's how you would use system libraries and fall back to embedding sources if the dependency is not available. ```meson @@ -115,9 +117,9 @@ if not libsimple_dep.found() libsimple_dep = libsimple_proj.get_variable('libsimple_dep') endif -executable('my_project', - 'my_project.cpp', - dependencies : libsimple_dep, +executable('my_project', + 'my_project.cpp', + dependencies : libsimple_dep, install : true) ``` @@ -141,14 +143,14 @@ project('my_project', 'cpp') libsimple_dep = dependency('libsimple', fallback : ['libsimple', 'libsimple_dep']) -executable('my_project', - 'my_project.cpp', - dependencies : libsimple_dep, +executable('my_project', + 'my_project.cpp', + dependencies : libsimple_dep, install : true) ``` With this setup when libsimple is provided by the system, we use it. When -that is not the case we use the embedded version (the one from subprojects). +that is not the case we use the embedded version (the one from subprojects). Note that `libsimple_dep` can point to an external or an internal dependency but you don't have to worry about their differences. Meson will take care diff --git a/docs/markdown/snippets/cmake_subprojects.md b/docs/markdown/snippets/cmake_subprojects.md new file mode 100644 index 000000000..07ff86882 --- /dev/null +++ b/docs/markdown/snippets/cmake_subprojects.md @@ -0,0 +1,30 @@ +## CMake subprojects + +Meson can now directly consume CMake based subprojects with the +CMake module. + +Using CMake subprojects is similar to using the "normal" meson +subprojects. They also have to be located in the `subprojects` +directory. + +Example: + +```cmake +add_library(cm_lib SHARED ${SOURCES}) +``` + +```meson +cmake = import('cmake') + +# Configure the CMake project +sub_proj = cmake.subproject('libsimple_cmake') + +# Fetch the dependency object +cm_lib = sub_proj.dependency('cm_lib') + +executable(exe1, ['sources'], dependencies: [cm_lib]) +``` + +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. diff --git a/mesonbuild/cmake/__init__.py b/mesonbuild/cmake/__init__.py new file mode 100644 index 000000000..d44932402 --- /dev/null +++ b/mesonbuild/cmake/__init__.py @@ -0,0 +1,26 @@ +# Copyright 2019 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 class contains the basic functionality needed to run any interpreter +# or an interpreter-based tool. + +__all__ = [ + 'CMakeClient', + 'CMakeException', + 'CMakeInterpreter', +] + +from .common import CMakeException +from .client import CMakeClient +from .interpreter import CMakeInterpreter diff --git a/mesonbuild/cmake/client.py b/mesonbuild/cmake/client.py new file mode 100644 index 000000000..f4b549b14 --- /dev/null +++ b/mesonbuild/cmake/client.py @@ -0,0 +1,512 @@ +# Copyright 2019 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 class contains the basic functionality needed to run any interpreter +# or an interpreter-based tool. + +from .common import CMakeException +from ..environment import Environment +from ..dependencies.base import CMakeDependency, ExternalProgram +from .. import mlog +from contextlib import contextmanager +from subprocess import Popen, PIPE, TimeoutExpired +from typing import List, Optional +import json +import os + +CMAKE_SERVER_BEGIN_STR = '[== "CMake Server" ==[' +CMAKE_SERVER_END_STR = ']== "CMake Server" ==]' + +CMAKE_MESSAGE_TYPES = { + 'error': ['cookie', 'errorMessage'], + 'hello': ['supportedProtocolVersions'], + 'message': ['cookie', 'message'], + 'progress': ['cookie'], + 'reply': ['cookie', 'inReplyTo'], + 'signal': ['cookie', 'name'], +} + +CMAKE_REPLY_TYPES = { + 'handshake': [], + 'configure': [], + 'compute': [], + 'cmakeInputs': ['buildFiles', 'cmakeRootDirectory', 'sourceDirectory'], + 'codemodel': ['configurations'] +} + +# Base CMake server message classes + +class MessageBase: + def __init__(self, msg_type: str, cookie: str): + self.type = msg_type + self.cookie = cookie + + def to_dict(self) -> dict: + return {'type': self.type, 'cookie': self.cookie} + + def log(self) -> None: + mlog.warning('CMake server message of type', mlog.bold(type(self).__name__), 'has no log function') + +class RequestBase(MessageBase): + cookie_counter = 0 + + def __init__(self, msg_type: str): + super().__init__(msg_type, self.gen_cookie()) + + @staticmethod + def gen_cookie(): + RequestBase.cookie_counter += 1 + return 'meson_{}'.format(RequestBase.cookie_counter) + +class ReplyBase(MessageBase): + def __init__(self, cookie: str, in_reply_to: str): + super().__init__('reply', cookie) + self.in_reply_to = in_reply_to + +class SignalBase(MessageBase): + def __init__(self, cookie: str, signal_name: str): + super().__init__('signal', cookie) + self.signal_name = signal_name + + def log(self) -> None: + mlog.log(mlog.bold('CMake signal:'), mlog.yellow(self.signal_name)) + +# Special Message classes + +class Error(MessageBase): + def __init__(self, cookie: str, message: str): + super().__init__('error', cookie) + self.message = message + + def log(self) -> None: + mlog.error(mlog.bold('CMake server error:'), mlog.red(self.message)) + +class Message(MessageBase): + def __init__(self, cookie: str, message: str): + super().__init__('message', cookie) + self.message = message + + def log(self) -> None: + #mlog.log(mlog.bold('CMake:'), self.message) + pass + +class Progress(MessageBase): + def __init__(self, cookie: str): + super().__init__('progress', cookie) + + def log(self) -> None: + pass + +class MessageHello(MessageBase): + def __init__(self, supported_protocol_versions: List[dict]): + super().__init__('hello', '') + self.supported_protocol_versions = supported_protocol_versions + + def supports(self, major: int, minor: Optional[int] = None) -> bool: + for i in self.supported_protocol_versions: + if major == i['major']: + if minor is None or minor == i['minor']: + return True + return False + +# Request classes + +class RequestHandShake(RequestBase): + def __init__(self, src_dir: str, build_dir: str, generator: str, vers_major: int, vers_minor: Optional[int] = None): + super().__init__('handshake') + self.src_dir = src_dir + self.build_dir = build_dir + self.generator = generator + self.vers_major = vers_major + self.vers_minor = vers_minor + + def to_dict(self) -> dict: + vers = {'major': self.vers_major} + if self.vers_minor is not None: + vers['minor'] = self.vers_minor + + # Old CMake versions (3.7) want '/' even on Windows + src_list = os.path.normpath(self.src_dir).split(os.sep) + bld_list = os.path.normpath(self.build_dir).split(os.sep) + + return { + **super().to_dict(), + 'sourceDirectory': '/'.join(src_list), + 'buildDirectory': '/'.join(bld_list), + 'generator': self.generator, + 'protocolVersion': vers + } + +class RequestConfigure(RequestBase): + def __init__(self, args: Optional[List[str]] = None): + super().__init__('configure') + self.args = args + + def to_dict(self) -> dict: + res = super().to_dict() + if self.args: + res['cacheArguments'] = self.args + return res + +class RequestCompute(RequestBase): + def __init__(self): + super().__init__('compute') + +class RequestCMakeInputs(RequestBase): + def __init__(self): + super().__init__('cmakeInputs') + +class RequestCodeModel(RequestBase): + def __init__(self): + super().__init__('codemodel') + +# Reply classes + +class ReplyHandShake(ReplyBase): + def __init__(self, cookie: str): + super().__init__(cookie, 'handshake') + +class ReplyConfigure(ReplyBase): + def __init__(self, cookie: str): + super().__init__(cookie, 'configure') + +class ReplyCompute(ReplyBase): + def __init__(self, cookie: str): + super().__init__(cookie, 'compute') + +class CMakeBuildFile: + def __init__(self, file: str, is_cmake: bool, is_temp: bool): + self.file = file + self.is_cmake = is_cmake + self.is_temp = is_temp + + def __repr__(self): + return '<{}: {}; cmake={}; temp={}>'.format(self.__class__.__name__, self.file, self.is_cmake, self.is_temp) + +class ReplyCMakeInputs(ReplyBase): + def __init__(self, cookie: str, cmake_root: str, src_dir: str, build_files: List[CMakeBuildFile]): + super().__init__(cookie, 'cmakeInputs') + self.cmake_root = cmake_root + self.src_dir = src_dir + self.build_files = build_files + + def log(self) -> None: + mlog.log('CMake root: ', mlog.bold(self.cmake_root)) + mlog.log('Source dir: ', mlog.bold(self.src_dir)) + mlog.log('Build files:', mlog.bold(str(len(self.build_files)))) + with mlog.nested(): + for i in self.build_files: + mlog.log(str(i)) + +def _flags_to_list(raw: str) -> List[str]: + # Convert a raw commandline string into a list of strings + res = [] + curr = '' + escape = False + in_string = False + for i in raw: + if escape: + # If the current char is not a quote, the '\' is probably important + if i not in ['"', "'"]: + curr += '\\' + curr += i + escape = False + elif i == '\\': + escape = True + elif i in ['"', "'"]: + in_string = not in_string + elif i in [' ', '\n']: + if in_string: + curr += i + else: + res += [curr] + curr = '' + else: + curr += i + res += [curr] + res = list(filter(lambda x: len(x) > 0, res)) + return res + +class CMakeFileGroup: + def __init__(self, data: dict): + self.defines = data.get('defines', '') + self.flags = _flags_to_list(data.get('compileFlags', '')) + self.includes = data.get('includePath', []) + self.is_generated = data.get('isGenerated', False) + self.language = data.get('language', 'C') + self.sources = data.get('sources', []) + + # Fix the include directories + tmp = [] + for i in self.includes: + if isinstance(i, dict) and 'path' in i: + tmp += [i['path']] + elif isinstance(i, str): + tmp += [i] + self.includes = tmp + + def log(self) -> None: + mlog.log('flags =', mlog.bold(', '.join(self.flags))) + mlog.log('defines =', mlog.bold(', '.join(self.defines))) + mlog.log('includes =', mlog.bold(', '.join(self.includes))) + mlog.log('is_generated =', mlog.bold('true' if self.is_generated else 'false')) + mlog.log('language =', mlog.bold(self.language)) + mlog.log('sources:') + for i in self.sources: + with mlog.nested(): + mlog.log(i) + +class CMakeTarget: + def __init__(self, data: dict): + self.artifacts = data.get('artifacts', []) + self.src_dir = data.get('sourceDirectory', '') + self.build_dir = data.get('buildDirectory', '') + self.name = data.get('name', '') + self.full_name = data.get('fullName', '') + self.install = data.get('hasInstallRule', False) + self.install_paths = list(set(data.get('installPaths', []))) + self.link_lang = data.get('linkerLanguage', '') + self.link_libraries = _flags_to_list(data.get('linkLibraries', '')) + self.link_flags = _flags_to_list(data.get('linkFlags', '')) + self.link_lang_flags = _flags_to_list(data.get('linkLanguageFlags', '')) + self.link_path = data.get('linkPath', '') + self.type = data.get('type', 'EXECUTABLE') + self.is_generator_provided = data.get('isGeneratorProvided', False) + self.files = [] + + for i in data.get('fileGroups', []): + self.files += [CMakeFileGroup(i)] + + def log(self) -> None: + mlog.log('artifacts =', mlog.bold(', '.join(self.artifacts))) + mlog.log('src_dir =', mlog.bold(self.src_dir)) + mlog.log('build_dir =', mlog.bold(self.build_dir)) + mlog.log('name =', mlog.bold(self.name)) + mlog.log('full_name =', mlog.bold(self.full_name)) + mlog.log('install =', mlog.bold('true' if self.install else 'false')) + mlog.log('install_paths =', mlog.bold(', '.join(self.install_paths))) + mlog.log('link_lang =', mlog.bold(self.link_lang)) + mlog.log('link_libraries =', mlog.bold(', '.join(self.link_libraries))) + mlog.log('link_flags =', mlog.bold(', '.join(self.link_flags))) + mlog.log('link_lang_flags =', mlog.bold(', '.join(self.link_lang_flags))) + mlog.log('link_path =', mlog.bold(self.link_path)) + mlog.log('type =', mlog.bold(self.type)) + mlog.log('is_generator_provided =', mlog.bold('true' if self.is_generator_provided else 'false')) + for idx, i in enumerate(self.files): + mlog.log('Files {}:'.format(idx)) + with mlog.nested(): + i.log() + +class CMakeProject: + def __init__(self, data: dict): + self.src_dir = data.get('sourceDirectory', '') + self.build_dir = data.get('buildDirectory', '') + self.name = data.get('name', '') + self.targets = [] + + for i in data.get('targets', []): + self.targets += [CMakeTarget(i)] + + def log(self) -> None: + mlog.log('src_dir =', mlog.bold(self.src_dir)) + mlog.log('build_dir =', mlog.bold(self.build_dir)) + mlog.log('name =', mlog.bold(self.name)) + for idx, i in enumerate(self.targets): + mlog.log('Target {}:'.format(idx)) + with mlog.nested(): + i.log() + +class CMakeConfiguration: + def __init__(self, data: dict): + self.name = data.get('name', '') + self.projects = [] + for i in data.get('projects', []): + self.projects += [CMakeProject(i)] + + def log(self) -> None: + mlog.log('name =', mlog.bold(self.name)) + for idx, i in enumerate(self.projects): + mlog.log('Project {}:'.format(idx)) + with mlog.nested(): + i.log() + +class ReplyCodeModel(ReplyBase): + def __init__(self, data: dict): + super().__init__(data['cookie'], 'codemodel') + self.configs = [] + for i in data['configurations']: + self.configs += [CMakeConfiguration(i)] + + def log(self) -> None: + mlog.log('CMake code mode:') + for idx, i in enumerate(self.configs): + mlog.log('Configuration {}:'.format(idx)) + with mlog.nested(): + i.log() + +# Main client class + +class CMakeClient: + def __init__(self, env: Environment): + self.env = env + self.proc = None + self.type_map = { + 'error': lambda data: Error(data['cookie'], data['errorMessage']), + 'hello': lambda data: MessageHello(data['supportedProtocolVersions']), + 'message': lambda data: Message(data['cookie'], data['message']), + 'progress': lambda data: Progress(data['cookie']), + 'reply': self.resolve_type_reply, + 'signal': lambda data: SignalBase(data['cookie'], data['name']) + } + + self.reply_map = { + 'handshake': lambda data: ReplyHandShake(data['cookie']), + 'configure': lambda data: ReplyConfigure(data['cookie']), + 'compute': lambda data: ReplyCompute(data['cookie']), + 'cmakeInputs': self.resolve_reply_cmakeInputs, + 'codemodel': lambda data: ReplyCodeModel(data), + } + + def readMessageRaw(self) -> dict: + assert(self.proc is not None) + rawData = [] + begin = False + while self.proc.poll() is None: + line = self.proc.stdout.readline() + if not line: + break + line = line.decode('utf-8') + line = line.strip() + + if begin and line == CMAKE_SERVER_END_STR: + break # End of the message + elif begin: + rawData += [line] + elif line == CMAKE_SERVER_BEGIN_STR: + begin = True # Begin of the message + + if rawData: + return json.loads('\n'.join(rawData)) + raise CMakeException('Failed to read data from the CMake server') + + def readMessage(self) -> MessageBase: + raw_data = self.readMessageRaw() + if 'type' not in raw_data: + raise CMakeException('The "type" attribute is missing from the message') + msg_type = raw_data['type'] + func = self.type_map.get(msg_type, None) + if not func: + raise CMakeException('Recieved unknown message type "{}"'.format(msg_type)) + for i in CMAKE_MESSAGE_TYPES[msg_type]: + if i not in raw_data: + raise CMakeException('Key "{}" is missing from CMake server message type {}'.format(i, msg_type)) + return func(raw_data) + + def writeMessage(self, msg: MessageBase) -> None: + raw_data = '\n{}\n{}\n{}\n'.format(CMAKE_SERVER_BEGIN_STR, json.dumps(msg.to_dict(), indent=2), CMAKE_SERVER_END_STR) + self.proc.stdin.write(raw_data.encode('ascii')) + self.proc.stdin.flush() + + def query(self, request: RequestBase) -> MessageBase: + self.writeMessage(request) + while True: + reply = self.readMessage() + if reply.cookie == request.cookie and reply.type in ['reply', 'error']: + return reply + + reply.log() + + def query_checked(self, request: RequestBase, message: str) -> ReplyBase: + reply = self.query(request) + h = mlog.green('SUCCEEDED') if reply.type == 'reply' else mlog.red('FAILED') + mlog.log(message + ':', h) + if reply.type != 'reply': + reply.log() + raise CMakeException('CMake server query failed') + return reply + + def do_handshake(self, src_dir: str, build_dir: str, generator: str, vers_major: int, vers_minor: Optional[int] = None) -> None: + # CMake prints the hello message on startup + msg = self.readMessage() + if not isinstance(msg, MessageHello): + raise CMakeException('Recieved an unexpected message from the CMake server') + + request = RequestHandShake(src_dir, build_dir, generator, vers_major, vers_minor) + self.query_checked(request, 'CMake server handshake') + + def resolve_type_reply(self, data: dict) -> ReplyBase: + reply_type = data['inReplyTo'] + func = self.reply_map.get(reply_type, None) + if not func: + raise CMakeException('Recieved unknown reply type "{}"'.format(reply_type)) + for i in ['cookie'] + CMAKE_REPLY_TYPES[reply_type]: + if i not in data: + raise CMakeException('Key "{}" is missing from CMake server message type {}'.format(i, type)) + return func(data) + + def resolve_reply_cmakeInputs(self, data: dict) -> ReplyCMakeInputs: + files = [] + for i in data['buildFiles']: + for j in i['sources']: + files += [CMakeBuildFile(j, i['isCMake'], i['isTemporary'])] + return ReplyCMakeInputs(data['cookie'], data['cmakeRootDirectory'], data['sourceDirectory'], files) + + @contextmanager + def connect(self): + self.startup() + try: + yield + finally: + self.shutdown() + + def startup(self) -> None: + if self.proc is not None: + raise CMakeException('The CMake server was already started') + + cmake_exe, cmake_vers, _ = CMakeDependency.find_cmake_binary(self.env) + if cmake_exe is None or cmake_exe is False: + raise CMakeException('Unable to find CMake') + assert(isinstance(cmake_exe, ExternalProgram)) + if not cmake_exe.found(): + raise CMakeException('Unable to find CMake') + + mlog.debug('Starting CMake server with CMake', mlog.bold(' '.join(cmake_exe.get_command())), 'version', mlog.cyan(cmake_vers)) + self.proc = Popen(cmake_exe.get_command() + ['-E', 'server', '--experimental', '--debug'], stdin=PIPE, stdout=PIPE) + + def shutdown(self) -> None: + if self.proc is None: + return + + mlog.debug('Shutting down the CMake server') + + # Close the pipes to exit + self.proc.stdin.close() + self.proc.stdout.close() + + # Wait for CMake to finish + try: + self.proc.wait(timeout=2) + except TimeoutExpired: + # Terminate CMake if there is a timeout + # terminate() may throw a platform specific exception if the process has already + # terminated. This may be the case if there is a race condition (CMake exited after + # the timeout but before the terminate() call). Additionally, this behavior can + # also be triggered on cygwin if CMake crashes. + # See https://github.com/mesonbuild/meson/pull/4969#issuecomment-499413233 + try: + self.proc.terminate() + except Exception: + pass + + self.proc = None diff --git a/mesonbuild/cmake/common.py b/mesonbuild/cmake/common.py new file mode 100644 index 000000000..217247e34 --- /dev/null +++ b/mesonbuild/cmake/common.py @@ -0,0 +1,21 @@ +# Copyright 2019 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 class contains the basic functionality needed to run any interpreter +# or an interpreter-based tool. + +from ..mesonlib import MesonException + +class CMakeException(MesonException): + pass diff --git a/mesonbuild/cmake/interpreter.py b/mesonbuild/cmake/interpreter.py new file mode 100644 index 000000000..406723f05 --- /dev/null +++ b/mesonbuild/cmake/interpreter.py @@ -0,0 +1,552 @@ +# Copyright 2019 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 class contains the basic functionality needed to run any interpreter +# or an interpreter-based tool. + +from .common import CMakeException +from .client import CMakeClient, RequestCMakeInputs, RequestConfigure, RequestCompute, RequestCodeModel, CMakeTarget +from .. import mlog +from ..build import Build +from ..environment import Environment +from ..mparser import Token, BaseNode, CodeBlockNode, FunctionNode, ArrayNode, ArgumentNode, AssignmentNode, BooleanNode, StringNode, IdNode, MethodNode +from ..backend.backends import Backend +from ..compilers.compilers import obj_suffixes +from ..dependencies.base import CMakeDependency, ExternalProgram +from subprocess import Popen, PIPE, STDOUT +from typing import List, Dict, Optional +import os, re + +backend_generator_map = { + 'ninja': 'Ninja', + 'xcode': 'Xcode', + 'vs2010': 'Visual Studio 10 2010', + 'vs2015': 'Visual Studio 15 2017', + 'vs2017': 'Visual Studio 15 2017', + 'vs2019': 'Visual Studio 16 2019', +} + +language_map = { + 'c': 'C', + 'cpp': 'CXX', + 'cuda': 'CUDA', + 'cs': 'CSharp', + 'java': 'Java', + 'fortran': 'Fortran', + 'swift': 'Swift', +} + +target_type_map = { + 'STATIC_LIBRARY': 'static_library', + 'MODULE_LIBRARY': 'shared_module', + 'SHARED_LIBRARY': 'shared_library', + 'EXECUTABLE': 'executable', + 'OBJECT_LIBRARY': 'static_library', +} + +skip_targets = ['UTILITY'] + +skip_input_extensions = ['.rule'] + +blacklist_compiler_flags = [ + '/W1', '/W2', '/W3', '/W4', '/Wall', + '/O1', '/O2', '/Ob', '/Od', '/Og', '/Oi', '/Os', '/Ot', '/Ox', '/Oy', '/Ob0', + '/RTC1', '/RTCc', '/RTCs', '/RTCu' +] + +blacklist_link_flags = [ + '/machine:x64', '/machine:x86', '/machine:arm', '/machine:ebc', + '/debug', '/debug:fastlink', '/debug:full', '/debug:none', + '/incremental', +] + +blacklist_clang_cl_link_flags = ['/GR', '/EHsc', '/MDd', '/Zi', '/RTC1'] + +blacklist_link_libs = [ + 'kernel32.lib', + 'user32.lib', + 'gdi32.lib', + 'winspool.lib', + 'shell32.lib', + 'ole32.lib', + 'oleaut32.lib', + 'uuid.lib', + 'comdlg32.lib', + 'advapi32.lib' +] + +class ConverterTarget: + lang_cmake_to_meson = {val.lower(): key for key, val in language_map.items()} + + def __init__(self, target: CMakeTarget, env: Environment): + self.env = env + self.artifacts = target.artifacts + self.src_dir = target.src_dir + self.build_dir = target.build_dir + self.name = target.name + self.full_name = target.full_name + self.type = target.type + self.install = target.install + self.install_dir = '' + self.link_libraries = target.link_libraries + self.link_flags = target.link_flags + target.link_lang_flags + + if target.install_paths: + self.install_dir = target.install_paths[0] + + self.languages = [] + self.sources = [] + self.generated = [] + self.includes = [] + self.link_with = [] + self.object_libs = [] + self.compile_opts = {} + self.pie = False + + # Project default override options (c_std, cpp_std, etc.) + self.override_options = [] + + for i in target.files: + # Determine the meson language + lang = ConverterTarget.lang_cmake_to_meson.get(i.language.lower(), 'c') + if lang not in self.languages: + self.languages += [lang] + if lang not in self.compile_opts: + self.compile_opts[lang] = [] + + # Add arguments, but avoid duplicates + args = i.flags + args += ['-D{}'.format(x) for x in i.defines] + self.compile_opts[lang] += [x for x in args if x not in self.compile_opts[lang]] + + # Handle include directories + self.includes += [x for x in i.includes if x not in self.includes] + + # Add sources to the right array + if i.is_generated: + self.generated += i.sources + else: + self.sources += i.sources + + def __repr__(self) -> str: + return '<{}: {}>'.format(self.__class__.__name__, self.name) + + std_regex = re.compile(r'([-]{1,2}std=|/std:v?|[-]{1,2}std:)(.*)') + + def postprocess(self, output_target_map: dict, root_src_dir: str, subdir: str, install_prefix: str) -> None: + # Detect setting the C and C++ standard + for i in ['c', 'cpp']: + if i not in self.compile_opts: + continue + + temp = [] + for j in self.compile_opts[i]: + m = ConverterTarget.std_regex.match(j) + if m: + self.override_options += ['{}_std={}'.format(i, m.group(2))] + elif j in ['-fPIC', '-fpic', '-fPIE', '-fpie']: + self.pie = True + elif j in blacklist_compiler_flags: + pass + else: + temp += [j] + + self.compile_opts[i] = temp + + # Make sure to force enable -fPIC for OBJECT libraries + if self.type.upper() == 'OBJECT_LIBRARY': + self.pie = True + + # Fix link libraries + temp = [] + for i in self.link_libraries: + # Let meson handle this arcane magic + if ',-rpath,' in i: + continue + if not os.path.isabs(i): + basename = os.path.basename(i) + if basename in output_target_map: + self.link_with += [output_target_map[basename]] + continue + + temp += [i] + self.link_libraries = temp + + # Make paths relative + def rel_path(x: str) -> str: + if not os.path.isabs(x): + x = os.path.normpath(os.path.join(self.src_dir, x)) + if os.path.isabs(x) and os.path.commonpath([x, root_src_dir]) == root_src_dir: + return os.path.relpath(x, root_src_dir) + if os.path.isabs(x) and os.path.commonpath([x, self.env.get_build_dir()]) == self.env.get_build_dir(): + return os.path.relpath(x, os.path.join(self.env.get_build_dir(), subdir)) + return x + + build_dir_rel = os.path.relpath(self.build_dir, os.path.join(self.env.get_build_dir(), subdir)) + self.includes = list(set([rel_path(x) for x in set(self.includes)] + [build_dir_rel])) + self.sources = [rel_path(x) for x in self.sources] + self.generated = [rel_path(x) for x in self.generated] + + # Filter out CMake rule files + self.sources = [x for x in self.sources if not any([x.endswith(y) for y in skip_input_extensions])] + self.generated = [x for x in self.generated if not any([x.endswith(y) for y in skip_input_extensions])] + + # Make sure '.' is always in the include directories + if '.' not in self.includes: + self.includes += ['.'] + + # make install dir relative to the install prefix + if self.install_dir and os.path.isabs(self.install_dir): + if os.path.commonpath([self.install_dir, install_prefix]) == install_prefix: + self.install_dir = os.path.relpath(self.install_dir, install_prefix) + + # Remove blacklisted options and libs + def check_flag(flag: str) -> bool: + if flag.lower() in blacklist_link_flags or flag in blacklist_compiler_flags + blacklist_clang_cl_link_flags: + return False + if flag.startswith('/D'): + return False + return True + + self.link_libraries = [x for x in self.link_libraries if x.lower() not in blacklist_link_libs] + self.link_flags = [x for x in self.link_flags if check_flag(x)] + + def process_object_libs(self, obj_target_list: List['ConverterTarget']): + # Try to detect the object library(s) from the generated input sources + temp = [os.path.basename(x) for x in self.generated] + temp = [x for x in temp if any([x.endswith('.' + y) for y in obj_suffixes])] + temp = [os.path.splitext(x)[0] for x in temp] + # Temp now stores the source filenames of the object files + for i in obj_target_list: + source_files = [os.path.basename(x) for x in i.sources + i.generated] + for j in source_files: + if j in temp: + self.object_libs += [i] + break + + # Filter out object files from the sources + self.generated = [x for x in self.generated if not any([x.endswith('.' + y) for y in obj_suffixes])] + + def meson_func(self) -> str: + return target_type_map.get(self.type.upper()) + + def log(self) -> None: + mlog.log('Target', mlog.bold(self.name)) + mlog.log(' -- artifacts: ', mlog.bold(str(self.artifacts))) + mlog.log(' -- full_name: ', mlog.bold(self.full_name)) + mlog.log(' -- type: ', mlog.bold(self.type)) + mlog.log(' -- install: ', mlog.bold('true' if self.install else 'false')) + mlog.log(' -- install_dir: ', mlog.bold(self.install_dir)) + mlog.log(' -- link_libraries: ', mlog.bold(str(self.link_libraries))) + mlog.log(' -- link_with: ', mlog.bold(str(self.link_with))) + mlog.log(' -- object_libs: ', mlog.bold(str(self.object_libs))) + mlog.log(' -- link_flags: ', mlog.bold(str(self.link_flags))) + mlog.log(' -- languages: ', mlog.bold(str(self.languages))) + mlog.log(' -- includes: ', mlog.bold(str(self.includes))) + mlog.log(' -- sources: ', mlog.bold(str(self.sources))) + mlog.log(' -- generated: ', mlog.bold(str(self.generated))) + mlog.log(' -- pie: ', mlog.bold('true' if self.pie else 'false')) + mlog.log(' -- override_opts: ', mlog.bold(str(self.override_options))) + mlog.log(' -- options:') + for key, val in self.compile_opts.items(): + mlog.log(' -', key, '=', mlog.bold(str(val))) + +class CMakeInterpreter: + def __init__(self, build: Build, subdir: str, src_dir: str, install_prefix: str, env: Environment, backend: Backend): + assert(hasattr(backend, 'name')) + self.build = build + self.subdir = subdir + self.src_dir = src_dir + self.build_dir_rel = os.path.join(subdir, '__CMake_build') + self.build_dir = os.path.join(env.get_build_dir(), self.build_dir_rel) + self.install_prefix = install_prefix + self.env = env + self.backend_name = backend.name + self.client = CMakeClient(self.env) + + # Raw CMake results + self.bs_files = [] + self.codemodel = None + + # Analysed data + self.project_name = '' + self.languages = [] + self.targets = [] + + # Generated meson data + self.generated_targets = {} + + def configure(self, extra_cmake_options: List[str]) -> None: + # Find CMake + cmake_exe, cmake_vers, _ = CMakeDependency.find_cmake_binary(self.env) + if cmake_exe is None or cmake_exe is False: + raise CMakeException('Unable to find CMake') + assert(isinstance(cmake_exe, ExternalProgram)) + if not cmake_exe.found(): + raise CMakeException('Unable to find CMake') + + generator = backend_generator_map[self.backend_name] + cmake_args = cmake_exe.get_command() + + # Map meson compiler to CMake variables + for lang, comp in self.env.coredata.compilers.items(): + if lang not in language_map: + continue + cmake_lang = language_map[lang] + exelist = comp.get_exelist() + if len(exelist) == 1: + cmake_args += ['-DCMAKE_{}_COMPILER={}'.format(cmake_lang, exelist[0])] + elif len(exelist) == 2: + cmake_args += ['-DCMAKE_{}_COMPILER_LAUNCHER={}'.format(cmake_lang, exelist[0]), + '-DCMAKE_{}_COMPILER={}'.format(cmake_lang, exelist[1])] + if hasattr(comp, 'get_linker_exelist') and comp.get_id() == 'clang-cl': + cmake_args += ['-DCMAKE_LINKER={}'.format(comp.get_linker_exelist()[0])] + cmake_args += ['-G', generator] + cmake_args += ['-DCMAKE_INSTALL_PREFIX={}'.format(self.install_prefix)] + cmake_args += extra_cmake_options + + # Run CMake + mlog.log() + with mlog.nested(): + mlog.log('Configuring the build directory with', mlog.bold('CMake'), 'version', mlog.cyan(cmake_vers)) + mlog.log(mlog.bold('Running:'), ' '.join(cmake_args)) + mlog.log() + os.makedirs(self.build_dir, exist_ok=True) + os_env = os.environ.copy() + os_env['LC_ALL'] = 'C' + proc = Popen(cmake_args + [self.src_dir], stdout=PIPE, stderr=STDOUT, cwd=self.build_dir, env=os_env) + + # Print CMake log in realtime + while True: + line = proc.stdout.readline() + if not line: + break + mlog.log(line.decode('utf-8').strip('\n')) + + # Wait for CMake to finish + proc.communicate() + + mlog.log() + h = mlog.green('SUCCEEDED') if proc.returncode == 0 else mlog.red('FAILED') + mlog.log('CMake configuration:', h) + if proc.returncode != 0: + raise CMakeException('Failed to configure the CMake subproject') + + def initialise(self, extra_cmake_options: List[str]) -> None: + # Run configure the old way becuse doing it + # with the server doesn't work for some reason + self.configure(extra_cmake_options) + + with self.client.connect(): + generator = backend_generator_map[self.backend_name] + self.client.do_handshake(self.src_dir, self.build_dir, generator, 1) + + # Do a second configure to initialise the server + self.client.query_checked(RequestConfigure(), 'CMake server configure') + + # Generate the build system files + self.client.query_checked(RequestCompute(), 'Generating build system files') + + # Get CMake build system files + bs_reply = self.client.query_checked(RequestCMakeInputs(), 'Querying build system files') + + # Now get the CMake code model + cm_reply = self.client.query_checked(RequestCodeModel(), 'Querying the CMake code model') + + src_dir = bs_reply.src_dir + self.bs_files = [x.file for x in bs_reply.build_files if not x.is_cmake and not x.is_temp] + self.bs_files = [os.path.relpath(os.path.join(src_dir, x), self.env.get_source_dir()) for x in self.bs_files] + self.bs_files = list(set(self.bs_files)) + self.codemodel = cm_reply + + def analyse(self) -> None: + if self.codemodel is None: + raise CMakeException('CMakeInterpreter was not initialized') + + # Clear analyser data + self.project_name = '' + self.languages = [] + self.targets = [] + + # Find all targets + for i in self.codemodel.configs: + for j in i.projects: + if not self.project_name: + self.project_name = j.name + for k in j.targets: + if k.type not in skip_targets: + self.targets += [ConverterTarget(k, self.env)] + + output_target_map = {x.full_name: x for x in self.targets} + for i in self.targets: + for j in i.artifacts: + output_target_map[os.path.basename(j)] = i + object_libs = [] + + # First pass: Basic target cleanup + for i in self.targets: + i.postprocess(output_target_map, self.src_dir, self.subdir, self.install_prefix) + if i.type == 'OBJECT_LIBRARY': + object_libs += [i] + self.languages += [x for x in i.languages if x not in self.languages] + + # Second pass: Detect object library dependencies + for i in self.targets: + i.process_object_libs(object_libs) + + mlog.log('CMake project', mlog.bold(self.project_name), 'has', mlog.bold(str(len(self.targets))), 'build targets.') + + def pretend_to_be_meson(self) -> CodeBlockNode: + if not self.project_name: + raise CMakeException('CMakeInterpreter was not analysed') + + def token(tid: str = 'string', val='') -> Token: + return Token(tid, self.subdir, 0, 0, 0, None, val) + + def string(value: str) -> StringNode: + return StringNode(token(val=value)) + + def id_node(value: str) -> IdNode: + return IdNode(token(val=value)) + + def nodeify(value): + if isinstance(value, str): + return string(value) + elif isinstance(value, bool): + return BooleanNode(token(), value) + elif isinstance(value, list): + return array(value) + return value + + def array(elements) -> ArrayNode: + args = ArgumentNode(token()) + if not isinstance(elements, list): + elements = [args] + args.arguments += [nodeify(x) for x in elements] + return ArrayNode(args, 0, 0, 0, 0) + + def function(name: str, args=None, kwargs=None) -> FunctionNode: + if args is None: + args = [] + if kwargs is None: + kwargs = {} + args_n = ArgumentNode(token()) + if not isinstance(args, list): + args = [args] + args_n.arguments = [nodeify(x) for x in args] + args_n.kwargs = {k: nodeify(v) for k, v in kwargs.items()} + func_n = FunctionNode(self.subdir, 0, 0, 0, 0, name, args_n) + return func_n + + def method(obj: BaseNode, name: str, args=None, kwargs=None) -> MethodNode: + if args is None: + args = [] + if kwargs is None: + kwargs = {} + args_n = ArgumentNode(token()) + if not isinstance(args, list): + args = [args] + args_n.arguments = [nodeify(x) for x in args] + args_n.kwargs = {k: nodeify(v) for k, v in kwargs.items()} + return MethodNode(self.subdir, 0, 0, obj, name, args_n) + + def assign(var_name: str, value: BaseNode) -> AssignmentNode: + return AssignmentNode(self.subdir, 0, 0, var_name, value) + + # Generate the root code block and the project function call + root_cb = CodeBlockNode(token()) + root_cb.lines += [function('project', [self.project_name] + self.languages)] + processed = {} + + def process_target(tgt: ConverterTarget): + # First handle inter target dependencies + link_with = [] + objec_libs = [] + for i in tgt.link_with: + assert(isinstance(i, ConverterTarget)) + if i.name not in processed: + process_target(i) + link_with += [id_node(processed[i.name]['tgt'])] + for i in tgt.object_libs: + assert(isinstance(i, ConverterTarget)) + if i.name not in processed: + process_target(i) + objec_libs += [processed[i.name]['tgt']] + + # Determine the meson function to use for the build target + tgt_func = tgt.meson_func() + if not tgt_func: + raise CMakeException('Unknown target type "{}"'.format(tgt.type)) + + # Determine the variable names + base_name = str(tgt.name) + base_name = base_name.replace('-', '_') + inc_var = '{}_inc'.format(base_name) + src_var = '{}_src'.format(base_name) + dep_var = '{}_dep'.format(base_name) + tgt_var = base_name + + # Generate target kwargs + tgt_kwargs = { + 'link_args': 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, + 'objects': [method(id_node(x), 'extract_all_objects') for x in objec_libs], + } + + # Handle compiler args + for key, val in tgt.compile_opts.items(): + tgt_kwargs['{}_args'.format(key)] = val + + # Handle -fPCI, etc + if tgt_func == 'executable': + tgt_kwargs['pie'] = tgt.pie + elif tgt_func == 'static_library': + tgt_kwargs['pic'] = tgt.pie + + # declare_dependency kwargs + dep_kwargs = { + 'link_args': tgt.link_flags + tgt.link_libraries, + 'link_with': id_node(tgt_var), + 'include_directories': id_node(inc_var), + } + + # Generate the function nodes + inc_node = assign(inc_var, function('include_directories', tgt.includes)) + src_node = assign(src_var, function('files', tgt.sources + tgt.generated)) + tgt_node = assign(tgt_var, function(tgt_func, [base_name, id_node(src_var)], tgt_kwargs)) + dep_node = assign(dep_var, function('declare_dependency', kwargs=dep_kwargs)) + + # Add the nodes to the ast + root_cb.lines += [inc_node, src_node, tgt_node, dep_node] + processed[tgt.name] = {'inc': inc_var, 'src': src_var, 'dep': dep_var, 'tgt': tgt_var, 'func': tgt_func} + + # Now generate the target function calls + for i in self.targets: + if i.name not in processed: + process_target(i) + + self.generated_targets = processed + return root_cb + + def target_info(self, target: str) -> Optional[Dict[str, str]]: + if target in self.generated_targets: + return self.generated_targets[target] + return None + + def target_list(self) -> List[str]: + return list(self.generated_targets.keys()) diff --git a/mesonbuild/dependencies/base.py b/mesonbuild/dependencies/base.py index a6fb0b6e9..9a142322c 100644 --- a/mesonbuild/dependencies/base.py +++ b/mesonbuild/dependencies/base.py @@ -1059,10 +1059,43 @@ class CMakeDependency(ExternalDependency): # List of successfully found modules self.found_modules = [] + self.cmakebin, self.cmakevers, for_machine = self.find_cmake_binary(environment, self.want_cross, self.silent) + if self.cmakebin is False: + self.cmakebin = None + msg = 'No CMake binary for machine %s not found. Giving up.' % for_machine + if self.required: + raise DependencyException(msg) + mlog.debug(msg) + return + + if CMakeDependency.class_cmakeinfo[for_machine] is None: + CMakeDependency.class_cmakeinfo[for_machine] = self._get_cmake_info() + self.cmakeinfo = CMakeDependency.class_cmakeinfo[for_machine] + if self.cmakeinfo is None: + raise self._gen_exception('Unable to obtain CMake system information') + + modules = [(x, True) for x in stringlistify(extract_as_list(kwargs, 'modules'))] + modules += [(x, False) for x in stringlistify(extract_as_list(kwargs, 'optional_modules'))] + cm_path = stringlistify(extract_as_list(kwargs, 'cmake_module_path')) + cm_path = [x if os.path.isabs(x) else os.path.join(environment.get_source_dir(), x) for x in cm_path] + cm_args = stringlistify(extract_as_list(kwargs, 'cmake_args')) + if cm_path: + cm_args.append('-DCMAKE_MODULE_PATH=' + ';'.join(cm_path)) + + pref_path = self.env.coredata.builtins_per_machine[for_machine]['cmake_prefix_path'].value + if pref_path: + cm_args.append('-DCMAKE_PREFIX_PATH={}'.format(';'.join(pref_path))) + + if not self._preliminary_find_check(name, cm_path, pref_path, environment.machines[for_machine]): + return + self._detect_dep(name, modules, cm_args) + + @staticmethod + def find_cmake_binary(environment: Environment, want_cross: bool = False, silent: bool = False) -> Tuple[str, str, MachineChoice]: # When finding dependencies for cross-compiling, we don't care about # the 'native' CMake binary # TODO: Test if this works as expected - if environment.is_cross_build() and not self.want_cross: + if environment.is_cross_build() and not want_cross: for_machine = MachineChoice.BUILD else: for_machine = MachineChoice.HOST @@ -1097,54 +1130,24 @@ class CMakeDependency(ExternalDependency): for potential_cmakebin in search(): mlog.debug('Trying CMake binary {} for machine {} at {}' .format(potential_cmakebin.name, for_machine, potential_cmakebin.command)) - version_if_ok = self.check_cmake(potential_cmakebin) + version_if_ok = CMakeDependency.check_cmake(potential_cmakebin) if not version_if_ok: continue - if not self.silent: + if not silent: mlog.log('Found CMake:', mlog.bold(potential_cmakebin.get_path()), '(%s)' % version_if_ok) CMakeDependency.class_cmakebin[for_machine] = potential_cmakebin CMakeDependency.class_cmakevers[for_machine] = version_if_ok break else: - if not self.silent: + if not silent: mlog.log('Found CMake:', mlog.red('NO')) # Set to False instead of None to signify that we've already # searched for it and not found it CMakeDependency.class_cmakebin[for_machine] = False CMakeDependency.class_cmakevers[for_machine] = None - self.cmakebin = CMakeDependency.class_cmakebin[for_machine] - self.cmakevers = CMakeDependency.class_cmakevers[for_machine] - if self.cmakebin is False: - self.cmakebin = None - msg = 'No CMake binary for machine %s not found. Giving up.' % for_machine - if self.required: - raise DependencyException(msg) - mlog.debug(msg) - return - - if CMakeDependency.class_cmakeinfo[for_machine] is None: - CMakeDependency.class_cmakeinfo[for_machine] = self._get_cmake_info() - self.cmakeinfo = CMakeDependency.class_cmakeinfo[for_machine] - if self.cmakeinfo is None: - raise self._gen_exception('Unable to obtain CMake system information') - - modules = [(x, True) for x in stringlistify(extract_as_list(kwargs, 'modules'))] - modules += [(x, False) for x in stringlistify(extract_as_list(kwargs, 'optional_modules'))] - cm_path = stringlistify(extract_as_list(kwargs, 'cmake_module_path')) - cm_path = [x if os.path.isabs(x) else os.path.join(environment.get_source_dir(), x) for x in cm_path] - cm_args = stringlistify(extract_as_list(kwargs, 'cmake_args')) - if cm_path: - cm_args.append('-DCMAKE_MODULE_PATH=' + ';'.join(cm_path)) - - pref_path = self.env.coredata.builtins_per_machine[for_machine]['cmake_prefix_path'].value - if pref_path: - cm_args.append('-DCMAKE_PREFIX_PATH={}'.format(';'.join(pref_path))) - - if not self._preliminary_find_check(name, cm_path, pref_path, environment.machines[for_machine]): - return - self._detect_dep(name, modules, cm_args) + return CMakeDependency.class_cmakebin[for_machine], CMakeDependency.class_cmakevers[for_machine], for_machine def __repr__(self): s = '<{0} {1}: {2} {3}>' @@ -1841,7 +1844,8 @@ set(CMAKE_SIZEOF_VOID_P "{}") def get_methods(): return [DependencyMethods.CMAKE] - def check_cmake(self, cmakebin): + @staticmethod + def check_cmake(cmakebin): if not cmakebin.found(): mlog.log('Did not find CMake {!r}'.format(cmakebin.name)) return None diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index a945f70ae..7c17e1cb3 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -31,6 +31,7 @@ from .interpreterbase import InterpreterObject, MutableInterpreterObject, Disabl from .interpreterbase import FeatureNew, FeatureDeprecated, FeatureNewKwargs from .interpreterbase import ObjectHolder from .modules import ModuleReturnValue +from .cmake import CMakeInterpreter import os, shutil, uuid import re, shlex @@ -2037,7 +2038,7 @@ permitted_kwargs = {'add_global_arguments': {'language', 'native'}, class Interpreter(InterpreterBase): def __init__(self, build, backend=None, subproject='', subdir='', subproject_dir='subprojects', - modules = None, default_project_options=None, mock=False): + modules = None, default_project_options=None, mock=False, ast=None): super().__init__(build.environment.get_source_dir(), subdir) self.an_unpicklable_object = mesonlib.an_unpicklable_object self.build = build @@ -2054,9 +2055,12 @@ class Interpreter(InterpreterBase): self.subproject_directory_name = subdir.split(os.path.sep)[-1] self.subproject_dir = subproject_dir self.option_file = os.path.join(self.source_root, self.subdir, 'meson_options.txt') - if not mock: + if not mock and ast is None: self.load_root_meson_file() self.sanity_check_ast() + elif ast is not None: + self.ast = ast + self.sanity_check_ast() self.builtin.update({'meson': MesonMain(build, self)}) self.generators = [] self.visited_subdirs = {} @@ -2243,7 +2247,7 @@ class Interpreter(InterpreterBase): raise InterpreterException('Stdlib definition for %s should have exactly two elements.' % l) projname, depname = di - subproj = self.do_subproject(projname, {}) + subproj = self.do_subproject(projname, 'meson', {}) self.build.cross_stdlibs[l] = subproj.get_variable_method([depname], {}) except KeyError: pass @@ -2418,13 +2422,13 @@ external dependencies (including libraries) must go to "dependencies".''') if len(args) != 1: raise InterpreterException('Subproject takes exactly one argument') dirname = args[0] - return self.do_subproject(dirname, kwargs) + return self.do_subproject(dirname, 'meson', kwargs) def disabled_subproject(self, dirname): self.subprojects[dirname] = SubprojectHolder(None, self.subproject_dir, dirname) return self.subprojects[dirname] - def do_subproject(self, dirname, kwargs): + def do_subproject(self, dirname: str, method: str, kwargs): disabled, required, feature = extract_required_kwarg(kwargs, self.subproject) if disabled: mlog.log('Subproject', mlog.bold(dirname), ':', 'skipped: feature', mlog.bold(feature), 'disabled') @@ -2457,7 +2461,7 @@ external dependencies (including libraries) must go to "dependencies".''') subproject_dir_abs = os.path.join(self.environment.get_source_dir(), self.subproject_dir) r = wrap.Resolver(subproject_dir_abs, self.coredata.get_builtin_option('wrap_mode')) try: - resolved = r.resolve(dirname) + resolved = r.resolve(dirname, method) except wrap.WrapException as e: subprojdir = os.path.join(self.subproject_dir, r.directory) if isinstance(e, wrap.WrapNotFoundException): @@ -2473,22 +2477,20 @@ external dependencies (including libraries) must go to "dependencies".''') raise e subdir = os.path.join(self.subproject_dir, resolved) + subdir_abs = os.path.join(subproject_dir_abs, resolved) os.makedirs(os.path.join(self.build.environment.get_build_dir(), subdir), exist_ok=True) self.global_args_frozen = True + mlog.log() with mlog.nested(): - mlog.log('Executing subproject', mlog.bold(dirname), '\n') + mlog.log('Executing subproject', mlog.bold(dirname), 'method', mlog.bold(method), '\n') try: - with mlog.nested(): - new_build = self.build.copy() - subi = Interpreter(new_build, self.backend, dirname, subdir, self.subproject_dir, - self.modules, default_options) - subi.subprojects = self.subprojects - - subi.subproject_stack = self.subproject_stack + [dirname] - current_active = self.active_projectname - subi.run() - mlog.log('Subproject', mlog.bold(dirname), 'finished.') + if method == 'meson': + return self._do_subproject_meson(dirname, subdir, default_options, required, kwargs) + elif method == 'cmake': + return self._do_subproject_cmake(dirname, subdir, subdir_abs, default_options, required, kwargs) + else: + raise InterpreterException('The method {} is invalid for the subproject {}'.format(method, dirname)) # Invalid code is always an error except InvalidCode: raise @@ -2502,6 +2504,18 @@ external dependencies (including libraries) must go to "dependencies".''') return self.disabled_subproject(dirname) raise e + def _do_subproject_meson(self, dirname, subdir, default_options, required, kwargs, ast=None, build_def_files=None): + with mlog.nested(): + new_build = self.build.copy() + subi = Interpreter(new_build, self.backend, dirname, subdir, self.subproject_dir, + self.modules, default_options, ast=ast) + subi.subprojects = self.subprojects + + subi.subproject_stack = self.subproject_stack + [dirname] + current_active = self.active_projectname + subi.run() + mlog.log('Subproject', mlog.bold(dirname), 'finished.') + mlog.log() if 'version' in kwargs: @@ -2513,11 +2527,48 @@ external dependencies (including libraries) must go to "dependencies".''') self.subprojects.update(subi.subprojects) self.subprojects[dirname] = SubprojectHolder(subi, self.subproject_dir, dirname) # Duplicates are possible when subproject uses files from project root - self.build_def_files = list(set(self.build_def_files + subi.build_def_files)) + if build_def_files: + self.build_def_files = list(set(self.build_def_files + build_def_files)) + else: + self.build_def_files = list(set(self.build_def_files + subi.build_def_files)) self.build.merge(subi.build) self.build.subprojects[dirname] = subi.project_version return self.subprojects[dirname] + def _do_subproject_cmake(self, dirname, subdir, subdir_abs, default_options, required, kwargs): + with mlog.nested(): + new_build = self.build.copy() + prefix = self.coredata.builtins['prefix'].value + cmake_options = mesonlib.stringlistify(kwargs.get('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() + + mlog.log() + with mlog.nested(): + mlog.log('Processing generated meson AST') + mlog.log() + + # Debug print the generated meson file + mlog.debug('=== BEGIN meson.build ===') + from .ast import AstIndentationGenerator, AstPrinter + printer = AstPrinter() + ast.accept(AstIndentationGenerator()) + ast.accept(printer) + printer.post_process() + mlog.debug(printer.result) + mlog.debug('=== END meson.build ===') + mlog.debug() + + result = self._do_subproject_meson(dirname, subdir, default_options, required, kwargs, ast, cm_int.bs_files) + result.cm_interpreter = cm_int + + mlog.log() + return result + def get_option_internal(self, optname): for opts in chain( [self.coredata.base_options, compilers.base_options, self.coredata.builtins], @@ -3105,7 +3156,7 @@ external dependencies (including libraries) must go to "dependencies".''') 'default_options': kwargs.get('default_options', []), 'required': kwargs.get('required', True), } - self.do_subproject(dirname, sp_kwargs) + self.do_subproject(dirname, 'meson', sp_kwargs) return self.get_subproject_dep(display_name, dirname, varname, kwargs) @FeatureNewKwargs('executable', '0.42.0', ['implib']) diff --git a/mesonbuild/modules/cmake.py b/mesonbuild/modules/cmake.py index d72ceca18..8ce5aef89 100644 --- a/mesonbuild/modules/cmake.py +++ b/mesonbuild/modules/cmake.py @@ -18,8 +18,8 @@ import shutil from . import ExtensionModule, ModuleReturnValue from .. import build, dependencies, mesonlib, mlog -from ..interpreterbase import permittedKwargs -from ..interpreter import ConfigurationDataHolder +from ..interpreterbase import permittedKwargs, FeatureNew, stringArgs, InterpreterObject, ObjectHolder +from ..interpreter import ConfigurationDataHolder, InterpreterException, SubprojectHolder COMPATIBILITIES = ['AnyNewerVersion', 'SameMajorVersion', 'SameMinorVersion', 'ExactVersion'] @@ -43,6 +43,62 @@ unset(_realOrig) unset(_realCurr) ''' +class CMakeSubprojectHolder(InterpreterObject, ObjectHolder): + def __init__(self, subp, pv): + assert(isinstance(subp, SubprojectHolder)) + assert(hasattr(subp, 'cm_interpreter')) + InterpreterObject.__init__(self) + ObjectHolder.__init__(self, subp, pv) + self.methods.update({'get_variable': self.get_variable, + 'dependency': self.dependency, + 'include_directories': self.include_directories, + 'target': self.target, + 'target_type': self.target_type, + 'target_list': self.target_list, + }) + + def _args_to_info(self, args): + if len(args) != 1: + raise InterpreterException('Exactly one argument is required.') + + tgt = args[0] + res = self.held_object.cm_interpreter.target_info(tgt) + if res is None: + raise InterpreterException('The CMake target {} does not exist'.format(tgt)) + + # Make sure that all keys are present (if not this is a bug) + assert(all([x in res for x in ['inc', 'src', 'dep', 'tgt', 'func']])) + return res + + @permittedKwargs({}) + def get_variable(self, args, kwargs): + return self.held_object.get_variable_method(args, kwargs) + + @permittedKwargs({}) + def dependency(self, args, kwargs): + info = self._args_to_info(args) + return self.get_variable([info['dep']], kwargs) + + @permittedKwargs({}) + def include_directories(self, args, kwargs): + info = self._args_to_info(args) + return self.get_variable([info['inc']], kwargs) + + @permittedKwargs({}) + def target(self, args, kwargs): + info = self._args_to_info(args) + return self.get_variable([info['tgt']], kwargs) + + @permittedKwargs({}) + def target_type(self, args, kwargs): + info = self._args_to_info(args) + return info['func'] + + @permittedKwargs({}) + def target_list(self, args, kwargs): + if len(args) > 0: + raise InterpreterException('target_list does not take any parameters.') + return self.held_object.cm_interpreter.target_list() class CmakeModule(ExtensionModule): cmake_detected = False @@ -51,6 +107,7 @@ class CmakeModule(ExtensionModule): def __init__(self, interpreter): super().__init__(interpreter) self.snippets.add('configure_package_config_file') + self.snippets.add('subproject') def detect_voidp_size(self, env): compilers = env.coredata.compilers @@ -210,5 +267,14 @@ class CmakeModule(ExtensionModule): return res + @FeatureNew('subproject', '0.51.0') + @permittedKwargs({'cmake_options'}) + @stringArgs + def subproject(self, interpreter, state, args, kwargs): + if len(args) != 1: + raise InterpreterException('Subproject takes exactly one argument') + dirname = args[0] + return CMakeSubprojectHolder(interpreter.do_subproject(dirname, 'cmake', kwargs), dirname) + def initialize(*args, **kwargs): return CmakeModule(*args, **kwargs) diff --git a/mesonbuild/msubprojects.py b/mesonbuild/msubprojects.py index 4162af6a0..bc6a6ced1 100755 --- a/mesonbuild/msubprojects.py +++ b/mesonbuild/msubprojects.py @@ -163,7 +163,7 @@ def download(wrap, repo_dir, options): return try: r = Resolver(os.path.dirname(repo_dir)) - r.resolve(wrap.name) + r.resolve(wrap.name, 'meson') mlog.log(' -> done') except WrapException as e: mlog.log(' ->', mlog.red(str(e))) diff --git a/mesonbuild/wrap/wrap.py b/mesonbuild/wrap/wrap.py index 3eb68a7f7..55f86bc05 100644 --- a/mesonbuild/wrap/wrap.py +++ b/mesonbuild/wrap/wrap.py @@ -111,7 +111,7 @@ class Resolver: self.subdir_root = subdir_root self.cachedir = os.path.join(self.subdir_root, 'packagecache') - def resolve(self, packagename): + def resolve(self, packagename: str, method: str): self.packagename = packagename self.directory = packagename # We always have to load the wrap file, if it exists, because it could @@ -123,9 +123,15 @@ class Resolver: raise WrapException('Directory key must be a name and not a path') self.dirname = os.path.join(self.subdir_root, self.directory) meson_file = os.path.join(self.dirname, 'meson.build') + cmake_file = os.path.join(self.dirname, 'CMakeLists.txt') + + if method not in ['meson', 'cmake']: + raise WrapException('Only the methods "meson" and "cmake" are supported') # The directory is there and has meson.build? Great, use it. - if os.path.exists(meson_file): + if method == 'meson' and os.path.exists(meson_file): + return self.directory + if method == 'cmake' and os.path.exists(cmake_file): return self.directory # Check if the subproject is a git submodule @@ -153,9 +159,11 @@ class Resolver: else: raise WrapException('Unknown wrap type {!r}'.format(self.wrap.type)) - # A meson.build file is required in the directory - if not os.path.exists(meson_file): + # A meson.build or CMakeLists.txt file is required in the directory + if method == 'meson' and not os.path.exists(meson_file): raise WrapException('Subproject exists but has no meson.build file') + if method == 'cmake' and not os.path.exists(cmake_file): + raise WrapException('Subproject exists but has no CMakeLists.txt file') return self.directory diff --git a/run_project_tests.py b/run_project_tests.py index a3aa07d53..f6d83b5c2 100755 --- a/run_project_tests.py +++ b/run_project_tests.py @@ -161,6 +161,11 @@ def platform_fix_name(fname, compiler, env): if not mesonlib.for_cygwin(env.is_cross_build(), env): return None + if fname.startswith('?!cygwin:'): + fname = fname[9:] + if mesonlib.for_cygwin(env.is_cross_build(), env): + return None + if fname.endswith('?so'): if mesonlib.for_windows(env.is_cross_build(), env) and canonical_compiler == 'msvc': fname = re.sub(r'lib/([^/]*)\?so$', r'bin/\1.dll', fname) @@ -222,6 +227,10 @@ def validate_install(srcdir, installdir, compiler, env): for fname in found: if fname not in expected: ret_msg += 'Extra file {0} found.\n'.format(fname) + if ret_msg != '': + ret_msg += '\nInstall dir contents:\n' + for i in found: + ret_msg += ' - {}'.format(i) return ret_msg def log_text_file(logfile, testdir, stdo, stde): @@ -553,6 +562,7 @@ def skip_csharp(backend): def detect_tests_to_run(): # Name, subdirectory, skip condition. all_tests = [ + ('cmake', 'cmake', not shutil.which('cmake') or (os.environ.get('compiler') == 'msvc2015' and under_ci)), ('common', 'common', False), ('warning-meson', 'warning', False), ('failing-meson', 'failing', False), @@ -664,6 +674,12 @@ def _run_tests(all_tests, log_name_base, failfast, extra_args): # print the meson log if available since it's a superset # of stdout and often has very useful information. failing_logs.append(result.mlog) + elif under_ci: + # Always print the complete meson log when running in + # a CI. This helps debugging issues that only occur in + # a hard to reproduce environment + failing_logs.append(result.mlog) + failing_logs.append(result.stdo) else: failing_logs.append(result.stdo) failing_logs.append(result.stde) diff --git a/setup.py b/setup.py index aab740e93..7a7edef70 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ entries = {'console_scripts': ['meson=mesonbuild.mesonmain:main']} packages = ['mesonbuild', 'mesonbuild.ast', 'mesonbuild.backend', + 'mesonbuild.cmake', 'mesonbuild.compilers', 'mesonbuild.dependencies', 'mesonbuild.modules', diff --git a/test cases/cmake/1 basic/main.cpp b/test cases/cmake/1 basic/main.cpp new file mode 100644 index 000000000..315c0f7dc --- /dev/null +++ b/test cases/cmake/1 basic/main.cpp @@ -0,0 +1,10 @@ +#include +#include + +using namespace std; + +int main() { + cmModClass obj("Hello"); + cout << obj.getStr() << endl; + return 0; +} diff --git a/test cases/cmake/1 basic/meson.build b/test cases/cmake/1 basic/meson.build new file mode 100644 index 000000000..a23063d24 --- /dev/null +++ b/test cases/cmake/1 basic/meson.build @@ -0,0 +1,12 @@ +project('cmakeSubTest', ['c', 'cpp']) + +cm = import('cmake') + +sub_pro = cm.subproject('cmMod') +sub_dep = sub_pro.dependency('cmModLib') + +assert(sub_pro.target_list() == ['cmModLib'], 'There should be exactly one target') +assert(sub_pro.target_type('cmModLib') == 'shared_library', 'Target type should be shared_library') + +exe1 = executable('main', ['main.cpp'], dependencies: [sub_dep]) +test('test1', exe1) diff --git a/test cases/cmake/1 basic/subprojects/cmMod/CMakeLists.txt b/test cases/cmake/1 basic/subprojects/cmMod/CMakeLists.txt new file mode 100644 index 000000000..7a9538b50 --- /dev/null +++ b/test cases/cmake/1 basic/subprojects/cmMod/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.5) + +project(cmMod) +set (CMAKE_CXX_STANDARD 14) + +add_definitions("-DDO_NOTHING_JUST_A_FLAG=1") + +add_library(cmModLib SHARED cmMod.cpp) +include(GenerateExportHeader) +generate_export_header(cmModLib) diff --git a/test cases/cmake/1 basic/subprojects/cmMod/cmMod.cpp b/test cases/cmake/1 basic/subprojects/cmMod/cmMod.cpp new file mode 100644 index 000000000..d3141d512 --- /dev/null +++ b/test cases/cmake/1 basic/subprojects/cmMod/cmMod.cpp @@ -0,0 +1,11 @@ +#include "cmMod.hpp" + +using namespace std; + +cmModClass::cmModClass(string foo) { + str = foo + " World"; +} + +string cmModClass::getStr() const { + return str; +} diff --git a/test cases/cmake/1 basic/subprojects/cmMod/cmMod.hpp b/test cases/cmake/1 basic/subprojects/cmMod/cmMod.hpp new file mode 100644 index 000000000..52f576bf3 --- /dev/null +++ b/test cases/cmake/1 basic/subprojects/cmMod/cmMod.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +#include "cmmodlib_export.h" + +class CMMODLIB_EXPORT cmModClass { + private: + std::string str; + public: + cmModClass(std::string foo); + + std::string getStr() const; +}; diff --git a/test cases/cmake/2 advanced/installed_files.txt b/test cases/cmake/2 advanced/installed_files.txt new file mode 100644 index 000000000..8cbdcd7b9 --- /dev/null +++ b/test cases/cmake/2 advanced/installed_files.txt @@ -0,0 +1,4 @@ +usr/?lib/libcmModLib?so +?cygwin:usr/lib/libcmModLib?implib +?!cygwin:usr/bin/libcmModLib?implib +usr/bin/testEXE?exe \ No newline at end of file diff --git a/test cases/cmake/2 advanced/main.cpp b/test cases/cmake/2 advanced/main.cpp new file mode 100644 index 000000000..6cc4c0ce3 --- /dev/null +++ b/test cases/cmake/2 advanced/main.cpp @@ -0,0 +1,15 @@ +#include +#include +#include "config.h" + +#if CONFIG_OPT != 42 +#error "Invalid value of CONFIG_OPT" +#endif + +using namespace std; + +int main() { + cmModClass obj("Hello"); + cout << obj.getStr() << endl; + return 0; +} diff --git a/test cases/cmake/2 advanced/meson.build b/test cases/cmake/2 advanced/meson.build new file mode 100644 index 000000000..385a49bfd --- /dev/null +++ b/test cases/cmake/2 advanced/meson.build @@ -0,0 +1,20 @@ +project('cmakeSubTest_advanced', ['c', 'cpp']) + +dep_test = dependency('ZLIB', method: 'cmake', required: false) +if not dep_test.found() + error('MESON_SKIP_TEST: zlib is not installed') +endif + +cm = import('cmake') + +# Test the "normal" subproject call +sub_pro = cm.subproject('cmMod') +sub_dep = sub_pro.dependency('cmModLib') + +# Build some files +exe1 = executable('main1', ['main.cpp'], dependencies: [sub_dep]) +test('test1', exe1) + +# Test if we can also extract executables +assert(sub_pro.target_type('testEXE') == 'executable', 'The type must be executable for obvious reasons') +test('test2', sub_pro.target('testEXE')) diff --git a/test cases/cmake/2 advanced/subprojects/cmMod/CMakeLists.txt b/test cases/cmake/2 advanced/subprojects/cmMod/CMakeLists.txt new file mode 100644 index 000000000..14908a366 --- /dev/null +++ b/test cases/cmake/2 advanced/subprojects/cmMod/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.5) + +project(cmMod) +set(CMAKE_CXX_STANDARD 14) + +find_package(ZLIB REQUIRED) + +include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/lib) + +set(CONFIG_OPT 42) +configure_file("config.h.in" "${CMAKE_CURRENT_BINARY_DIR}/config.h" @ONLY) + +add_library(cmModLib SHARED lib/cmMod.cpp) +include(GenerateExportHeader) +generate_export_header(cmModLib) + +add_executable(testEXE main.cpp) + +target_link_libraries(cmModLib ZLIB::ZLIB) +target_link_libraries(testEXE cmModLib) + +install(TARGETS cmModLib testEXE LIBRARY DESTINATION lib RUNTIME DESTINATION bin) diff --git a/test cases/cmake/2 advanced/subprojects/cmMod/config.h.in b/test cases/cmake/2 advanced/subprojects/cmMod/config.h.in new file mode 100644 index 000000000..f538ac985 --- /dev/null +++ b/test cases/cmake/2 advanced/subprojects/cmMod/config.h.in @@ -0,0 +1,3 @@ +#pragma once + +#define CONFIG_OPT @CONFIG_OPT@ diff --git a/test cases/cmake/2 advanced/subprojects/cmMod/lib/cmMod.cpp b/test cases/cmake/2 advanced/subprojects/cmMod/lib/cmMod.cpp new file mode 100644 index 000000000..027296e9c --- /dev/null +++ b/test cases/cmake/2 advanced/subprojects/cmMod/lib/cmMod.cpp @@ -0,0 +1,17 @@ +#include "cmMod.hpp" +#include +#include "config.h" + +#if CONFIG_OPT != 42 +#error "Invalid value of CONFIG_OPT" +#endif + +using namespace std; + +cmModClass::cmModClass(string foo) { + str = foo + " World " + zlibVersion(); +} + +string cmModClass::getStr() const { + return str; +} diff --git a/test cases/cmake/2 advanced/subprojects/cmMod/lib/cmMod.hpp b/test cases/cmake/2 advanced/subprojects/cmMod/lib/cmMod.hpp new file mode 100644 index 000000000..52f576bf3 --- /dev/null +++ b/test cases/cmake/2 advanced/subprojects/cmMod/lib/cmMod.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +#include "cmmodlib_export.h" + +class CMMODLIB_EXPORT cmModClass { + private: + std::string str; + public: + cmModClass(std::string foo); + + std::string getStr() const; +}; diff --git a/test cases/cmake/2 advanced/subprojects/cmMod/main.cpp b/test cases/cmake/2 advanced/subprojects/cmMod/main.cpp new file mode 100644 index 000000000..a1b1637fe --- /dev/null +++ b/test cases/cmake/2 advanced/subprojects/cmMod/main.cpp @@ -0,0 +1,11 @@ +#include +#include +#include "lib/cmMod.hpp" + +using namespace std; + +int main() { + cmModClass obj("Hello (LIB TEST)"); + cout << obj.getStr() << " ZLIB: " << zlibVersion() << endl; + return 0; +} diff --git a/test cases/cmake/3 advanced no dep/installed_files.txt b/test cases/cmake/3 advanced no dep/installed_files.txt new file mode 100644 index 000000000..e16e27d78 --- /dev/null +++ b/test cases/cmake/3 advanced no dep/installed_files.txt @@ -0,0 +1,6 @@ +usr/?lib/libcmModLib?so +?cygwin:usr/lib/libcmModLib?implib +?!cygwin:usr/bin/libcmModLib?implib +?msvc:usr/bin/cmModLib.pdb +?msvc:usr/bin/testEXE.pdb +usr/bin/testEXE?exe \ No newline at end of file diff --git a/test cases/cmake/3 advanced no dep/main.cpp b/test cases/cmake/3 advanced no dep/main.cpp new file mode 100644 index 000000000..6cc4c0ce3 --- /dev/null +++ b/test cases/cmake/3 advanced no dep/main.cpp @@ -0,0 +1,15 @@ +#include +#include +#include "config.h" + +#if CONFIG_OPT != 42 +#error "Invalid value of CONFIG_OPT" +#endif + +using namespace std; + +int main() { + cmModClass obj("Hello"); + cout << obj.getStr() << endl; + return 0; +} diff --git a/test cases/cmake/3 advanced no dep/meson.build b/test cases/cmake/3 advanced no dep/meson.build new file mode 100644 index 000000000..c10dbf51d --- /dev/null +++ b/test cases/cmake/3 advanced no dep/meson.build @@ -0,0 +1,15 @@ +project('cmakeSubTest_advanced', ['c', 'cpp']) + +cm = import('cmake') + +# Test the "normal" subproject call +sub_pro = cm.subproject('cmMod') +sub_dep = sub_pro.dependency('cmModLib') + +# Build some files +exe1 = executable('main1', ['main.cpp'], dependencies: [sub_dep]) +test('test1', exe1) + +# Test if we can also extract executables +assert(sub_pro.target_type('testEXE') == 'executable', 'The type must be executable for obvious reasons') +test('test2', sub_pro.target('testEXE')) diff --git a/test cases/cmake/3 advanced no dep/subprojects/cmMod/CMakeLists.txt b/test cases/cmake/3 advanced no dep/subprojects/cmMod/CMakeLists.txt new file mode 100644 index 000000000..57f0a644b --- /dev/null +++ b/test cases/cmake/3 advanced no dep/subprojects/cmMod/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.5) + +project(cmMod) +set(CMAKE_CXX_STANDARD 14) + +include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/lib) + +set(CONFIG_OPT 42) +configure_file("config.h.in" "${CMAKE_CURRENT_BINARY_DIR}/config.h" @ONLY) + +add_library(cmModLib SHARED lib/cmMod.cpp) +include(GenerateExportHeader) +generate_export_header(cmModLib) + +add_executable(testEXE main.cpp) + +target_link_libraries(testEXE cmModLib) + +install(TARGETS cmModLib testEXE LIBRARY DESTINATION lib RUNTIME DESTINATION bin) diff --git a/test cases/cmake/3 advanced no dep/subprojects/cmMod/config.h.in b/test cases/cmake/3 advanced no dep/subprojects/cmMod/config.h.in new file mode 100644 index 000000000..f538ac985 --- /dev/null +++ b/test cases/cmake/3 advanced no dep/subprojects/cmMod/config.h.in @@ -0,0 +1,3 @@ +#pragma once + +#define CONFIG_OPT @CONFIG_OPT@ diff --git a/test cases/cmake/3 advanced no dep/subprojects/cmMod/lib/cmMod.cpp b/test cases/cmake/3 advanced no dep/subprojects/cmMod/lib/cmMod.cpp new file mode 100644 index 000000000..741e8dff0 --- /dev/null +++ b/test cases/cmake/3 advanced no dep/subprojects/cmMod/lib/cmMod.cpp @@ -0,0 +1,16 @@ +#include "cmMod.hpp" +#include "config.h" + +#if CONFIG_OPT != 42 +#error "Invalid value of CONFIG_OPT" +#endif + +using namespace std; + +cmModClass::cmModClass(string foo) { + str = foo + " World"; +} + +string cmModClass::getStr() const { + return str; +} diff --git a/test cases/cmake/3 advanced no dep/subprojects/cmMod/lib/cmMod.hpp b/test cases/cmake/3 advanced no dep/subprojects/cmMod/lib/cmMod.hpp new file mode 100644 index 000000000..52f576bf3 --- /dev/null +++ b/test cases/cmake/3 advanced no dep/subprojects/cmMod/lib/cmMod.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +#include "cmmodlib_export.h" + +class CMMODLIB_EXPORT cmModClass { + private: + std::string str; + public: + cmModClass(std::string foo); + + std::string getStr() const; +}; diff --git a/test cases/cmake/3 advanced no dep/subprojects/cmMod/main.cpp b/test cases/cmake/3 advanced no dep/subprojects/cmMod/main.cpp new file mode 100644 index 000000000..cd2104217 --- /dev/null +++ b/test cases/cmake/3 advanced no dep/subprojects/cmMod/main.cpp @@ -0,0 +1,10 @@ +#include +#include "lib/cmMod.hpp" + +using namespace std; + +int main() { + cmModClass obj("Hello (LIB TEST)"); + cout << obj.getStr() << endl; + return 0; +} diff --git a/test cases/cmake/4 code gen/main.cpp b/test cases/cmake/4 code gen/main.cpp new file mode 100644 index 000000000..3ddbf636b --- /dev/null +++ b/test cases/cmake/4 code gen/main.cpp @@ -0,0 +1,8 @@ +#include +#include "test.hpp" + +using namespace std; + +int main() { + cout << getStr() << endl; +} diff --git a/test cases/cmake/4 code gen/meson.build b/test cases/cmake/4 code gen/meson.build new file mode 100644 index 000000000..592f903c8 --- /dev/null +++ b/test cases/cmake/4 code gen/meson.build @@ -0,0 +1,20 @@ +project('cmake_code_gen', ['c', 'cpp']) + +cm = import('cmake') + +# Subproject with the "code generator" +sub_pro = cm.subproject('cmCodeGen') +sub_exe = sub_pro.target('genA') + +# Generate the source +generated = custom_target( + 'cmake-generated', + input: [], + output: ['test.cpp'], + command: [sub_exe, '@OUTPUT@'] +) + +# Build the exe +exe1 = executable('main1', ['main.cpp', generated]) + +test('test1', exe1) diff --git a/test cases/cmake/4 code gen/subprojects/cmCodeGen/CMakeLists.txt b/test cases/cmake/4 code gen/subprojects/cmCodeGen/CMakeLists.txt new file mode 100644 index 000000000..268743c73 --- /dev/null +++ b/test cases/cmake/4 code gen/subprojects/cmCodeGen/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.7) + +set(CMAKE_CXX_STANDARD 14) + +add_executable(genA main.cpp) diff --git a/test cases/cmake/4 code gen/subprojects/cmCodeGen/main.cpp b/test cases/cmake/4 code gen/subprojects/cmCodeGen/main.cpp new file mode 100644 index 000000000..5b7fed2b9 --- /dev/null +++ b/test cases/cmake/4 code gen/subprojects/cmCodeGen/main.cpp @@ -0,0 +1,21 @@ +#include +#include + +using namespace std; + +int main(int argc, const char *argv[]) { + if(argc < 2) { + cerr << argv[0] << " requires an output file!" << endl; + return 1; + } + ofstream out(argv[1]); + out << R"( +#include "test.hpp" + +std::string getStr() { + return "Hello World"; +} +)"; + + return 0; +} diff --git a/test cases/cmake/4 code gen/test.hpp b/test cases/cmake/4 code gen/test.hpp new file mode 100644 index 000000000..8e25a0a5e --- /dev/null +++ b/test cases/cmake/4 code gen/test.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include + +std::string getStr(); diff --git a/test cases/cmake/5 object library/main.cpp b/test cases/cmake/5 object library/main.cpp new file mode 100644 index 000000000..f3836087b --- /dev/null +++ b/test cases/cmake/5 object library/main.cpp @@ -0,0 +1,9 @@ +#include +#include "libA.hpp" +#include "libB.hpp" + +using namespace std; + +int main() { + cout << getLibStr() << " -- " << getZlibVers() << endl; +} diff --git a/test cases/cmake/5 object library/meson.build b/test cases/cmake/5 object library/meson.build new file mode 100644 index 000000000..268c2bea2 --- /dev/null +++ b/test cases/cmake/5 object library/meson.build @@ -0,0 +1,25 @@ +project('cmake_object_lib_test', ['c', 'cpp']) + +dep_test = dependency('ZLIB', method: 'cmake', required: false) +if not dep_test.found() + error('MESON_SKIP_TEST: zlib is not installed') +endif + +if build_machine.system() == 'windows' + error('MESON_SKIP_TEST: Windows is not supported because of symbol export problems') +endif + +cm = import('cmake') + +sub_pro = cm.subproject('cmObjLib') +sub_sha = sub_pro.dependency('lib_sha') +sub_sta = sub_pro.dependency('lib_sta') + +# Required for the static library +zlib_dep = dependency('zlib') + +exe_sha = executable('shared', ['main.cpp'], dependencies: [sub_sha]) +exe_sta = executable('static', ['main.cpp'], dependencies: [sub_sta, zlib_dep]) + +test('test1', exe_sha) +test('test1', exe_sta) diff --git a/test cases/cmake/5 object library/subprojects/cmObjLib/CMakeLists.txt b/test cases/cmake/5 object library/subprojects/cmObjLib/CMakeLists.txt new file mode 100644 index 000000000..ee9be47e5 --- /dev/null +++ b/test cases/cmake/5 object library/subprojects/cmObjLib/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.7) + +find_package(ZLIB REQUIRED) + +add_library(lib_obj OBJECT libA.cpp libB.cpp) +add_library(lib_sha SHARED $) +add_library(lib_sta STATIC $) + +target_link_libraries(lib_sha ZLIB::ZLIB) +target_link_libraries(lib_sta ZLIB::ZLIB) diff --git a/test cases/cmake/5 object library/subprojects/cmObjLib/libA.cpp b/test cases/cmake/5 object library/subprojects/cmObjLib/libA.cpp new file mode 100644 index 000000000..3736b2c2d --- /dev/null +++ b/test cases/cmake/5 object library/subprojects/cmObjLib/libA.cpp @@ -0,0 +1,5 @@ +#include "libA.hpp" + +std::string getLibStr() { + return "Hello World"; +} diff --git a/test cases/cmake/5 object library/subprojects/cmObjLib/libA.hpp b/test cases/cmake/5 object library/subprojects/cmObjLib/libA.hpp new file mode 100644 index 000000000..58c941321 --- /dev/null +++ b/test cases/cmake/5 object library/subprojects/cmObjLib/libA.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include + +std::string getLibStr(); diff --git a/test cases/cmake/5 object library/subprojects/cmObjLib/libB.cpp b/test cases/cmake/5 object library/subprojects/cmObjLib/libB.cpp new file mode 100644 index 000000000..b359c298e --- /dev/null +++ b/test cases/cmake/5 object library/subprojects/cmObjLib/libB.cpp @@ -0,0 +1,6 @@ +#include "libB.hpp" +#include + +std::string getZlibVers() { + return zlibVersion(); +} diff --git a/test cases/cmake/5 object library/subprojects/cmObjLib/libB.hpp b/test cases/cmake/5 object library/subprojects/cmObjLib/libB.hpp new file mode 100644 index 000000000..71db6b7a3 --- /dev/null +++ b/test cases/cmake/5 object library/subprojects/cmObjLib/libB.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include + +std::string getZlibVers(); diff --git a/test cases/cmake/6 object library no dep/main.cpp b/test cases/cmake/6 object library no dep/main.cpp new file mode 100644 index 000000000..f3836087b --- /dev/null +++ b/test cases/cmake/6 object library no dep/main.cpp @@ -0,0 +1,9 @@ +#include +#include "libA.hpp" +#include "libB.hpp" + +using namespace std; + +int main() { + cout << getLibStr() << " -- " << getZlibVers() << endl; +} diff --git a/test cases/cmake/6 object library no dep/meson.build b/test cases/cmake/6 object library no dep/meson.build new file mode 100644 index 000000000..7494fee9e --- /dev/null +++ b/test cases/cmake/6 object library no dep/meson.build @@ -0,0 +1,17 @@ +project('cmake_object_lib_test', ['c', 'cpp']) + +if build_machine.system() == 'windows' + error('MESON_SKIP_TEST: Windows is not supported because of symbol export problems') +endif + +cm = import('cmake') + +sub_pro = cm.subproject('cmObjLib') +sub_sha = sub_pro.dependency('lib_sha') +sub_sta = sub_pro.dependency('lib_sta') + +exe_sha = executable('shared', ['main.cpp'], dependencies: [sub_sha]) +exe_sta = executable('static', ['main.cpp'], dependencies: [sub_sta]) + +test('test1', exe_sha) +test('test1', exe_sta) diff --git a/test cases/cmake/6 object library no dep/subprojects/cmObjLib/CMakeLists.txt b/test cases/cmake/6 object library no dep/subprojects/cmObjLib/CMakeLists.txt new file mode 100644 index 000000000..08c6a1848 --- /dev/null +++ b/test cases/cmake/6 object library no dep/subprojects/cmObjLib/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.7) + +add_library(lib_obj OBJECT libA.cpp libB.cpp) +add_library(lib_sha SHARED $) +add_library(lib_sta STATIC $) diff --git a/test cases/cmake/6 object library no dep/subprojects/cmObjLib/libA.cpp b/test cases/cmake/6 object library no dep/subprojects/cmObjLib/libA.cpp new file mode 100644 index 000000000..3736b2c2d --- /dev/null +++ b/test cases/cmake/6 object library no dep/subprojects/cmObjLib/libA.cpp @@ -0,0 +1,5 @@ +#include "libA.hpp" + +std::string getLibStr() { + return "Hello World"; +} diff --git a/test cases/cmake/6 object library no dep/subprojects/cmObjLib/libA.hpp b/test cases/cmake/6 object library no dep/subprojects/cmObjLib/libA.hpp new file mode 100644 index 000000000..58c941321 --- /dev/null +++ b/test cases/cmake/6 object library no dep/subprojects/cmObjLib/libA.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include + +std::string getLibStr(); diff --git a/test cases/cmake/6 object library no dep/subprojects/cmObjLib/libB.cpp b/test cases/cmake/6 object library no dep/subprojects/cmObjLib/libB.cpp new file mode 100644 index 000000000..187d10f39 --- /dev/null +++ b/test cases/cmake/6 object library no dep/subprojects/cmObjLib/libB.cpp @@ -0,0 +1,5 @@ +#include "libB.hpp" + +std::string getZlibVers() { + return "STUB"; +} diff --git a/test cases/cmake/6 object library no dep/subprojects/cmObjLib/libB.hpp b/test cases/cmake/6 object library no dep/subprojects/cmObjLib/libB.hpp new file mode 100644 index 000000000..71db6b7a3 --- /dev/null +++ b/test cases/cmake/6 object library no dep/subprojects/cmObjLib/libB.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include + +std::string getZlibVers(); diff --git a/test cases/cmake/7 cmake options/meson.build b/test cases/cmake/7 cmake options/meson.build new file mode 100644 index 000000000..8bb6d1d5c --- /dev/null +++ b/test cases/cmake/7 cmake options/meson.build @@ -0,0 +1,3 @@ +project('cmake_set_opt', ['c', 'cpp']) + +import('cmake').subproject('cmOpts', cmake_options: '-DSOME_CMAKE_VAR=something') diff --git a/test cases/cmake/7 cmake options/subprojects/cmOpts/CMakeLists.txt b/test cases/cmake/7 cmake options/subprojects/cmOpts/CMakeLists.txt new file mode 100644 index 000000000..62b5990e5 --- /dev/null +++ b/test cases/cmake/7 cmake options/subprojects/cmOpts/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.7) + +if(NOT "${SOME_CMAKE_VAR}" STREQUAL "something") + message(FATAL_ERROR "Setting the CMake var failed") +endif()