diff options
48 files changed, 1227 insertions, 203 deletions
@@ -0,0 +1,33 @@ +[flake8] +ignore = + # E241: multiple spaces after ':' + E241, + # E251: unexpected spaces around keyword / parameter equals + E251, + # E261: at least two spaces before inline comment + E261, + # E265: block comment should start with '# ' + E265, + # E501: line too long + E501, + # E302: expected 2 blank lines, found 1 + E302, + # E305: expected 2 blank lines after class or function definition, found 1 + E305, + # E401: multiple imports on one line + E401, + # E266: too many leading '#' for block comment + E266, + # E402: module level import not at top of file + E402, + # E731: do not assign a lambda expression, use a def (too many false positives) + E731, + # E741: ambiguous variable name 'l' + E741, + # E722: do not use bare except' + E722, + # W504: line break after binary operator + W504, + # A003: builtin class attribute + A003 +max-line-length = 120 diff --git a/.travis.yml b/.travis.yml index 7658fa0..8f393f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ compiler: env: - MESON_ARGS="" - - MESON_ARGS="--unity=on" + - RUN_TESTS_ARGS="--no-unittests" MESON_ARGS="--unity=on" language: - cpp @@ -63,4 +63,4 @@ script: /bin/sh -c "cd /root && mkdir -p tools; wget -c http://nirbheek.in/files/binaries/ninja/linux-amd64/ninja -O /root/tools/ninja; chmod +x /root/tools/ninja; CC=$CC CXX=$CXX OBJC=$CC OBJCXX=$CXX PATH=/root/tools:$PATH MESON_FIXED_NINJA=1 ./run_tests.py $RUN_TESTS_ARGS -- $MESON_ARGS && chmod -R a+rwX .coverage" fi # Ensure that llvm is added after $PATH, otherwise the clang from that llvm install will be used instead of the native apple clang. - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then SDKROOT=$(xcodebuild -version -sdk macosx Path) CPPFLAGS=-I/usr/local/include LDFLAGS=-L/usr/local/lib OBJC=$CC OBJCXX=$CXX PATH=$HOME/tools:/usr/local/opt/qt/bin:$PATH:$(brew --prefix llvm)/bin MESON_FIXED_NINJA=1 ./run_tests.py --backend=ninja -- $MESON_ARGS ; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then SDKROOT=$(xcodebuild -version -sdk macosx Path) CPPFLAGS=-I/usr/local/include LDFLAGS=-L/usr/local/lib OBJC=$CC OBJCXX=$CXX PATH=$HOME/tools:/usr/local/opt/qt/bin:$PATH:$(brew --prefix llvm)/bin MESON_FIXED_NINJA=1 ./run_tests.py $RUN_TESTS_ARGS --backend=ninja -- $MESON_ARGS ; fi diff --git a/ci/azure-steps.yml b/ci/azure-steps.yml index 77f61fe..36e6fb4 100644 --- a/ci/azure-steps.yml +++ b/ci/azure-steps.yml @@ -154,6 +154,10 @@ steps: where.exe python python --version + # Needed for running unit tests in parallel. + python -m pip install --upgrade pytest-xdist + + echo "" echo "Locating cl, rc:" where.exe cl diff --git a/ciimage/Dockerfile b/ciimage/Dockerfile index d5f4816..585a6ef 100644 --- a/ciimage/Dockerfile +++ b/ciimage/Dockerfile @@ -7,6 +7,7 @@ ENV DC=gdc RUN sed -i '/^#\sdeb-src /s/^#//' "/etc/apt/sources.list" \ && apt-get -y update && apt-get -y upgrade \ && apt-get -y build-dep meson \ +&& apt-get -y install python3-pytest-xdist \ && apt-get -y install python3-pip libxml2-dev libxslt1-dev cmake libyaml-dev \ && python3 -m pip install hotdoc codecov \ && apt-get -y install wget unzip \ @@ -22,6 +23,7 @@ RUN sed -i '/^#\sdeb-src /s/^#//' "/etc/apt/sources.list" \ && apt-get -y install libgcrypt11-dev \ && apt-get -y install libgpgme-dev \ && apt-get -y install libhdf5-dev \ +&& apt-get -y install libboost-python-dev \ && dub fetch urld && dub build urld --compiler=gdc \ && dub fetch dubtestproject \ && dub build dubtestproject:test1 --compiler=ldc2 \ diff --git a/data/syntax-highlighting/vim/syntax/meson.vim b/data/syntax-highlighting/vim/syntax/meson.vim index 85acf43..94936c8 100644 --- a/data/syntax-highlighting/vim/syntax/meson.vim +++ b/data/syntax-highlighting/vim/syntax/meson.vim @@ -69,6 +69,7 @@ syn keyword mesonBuiltin \ add_project_arguments \ add_project_link_arguments \ add_test_setup + \ alias_target \ assert \ benchmark \ both_libraries diff --git a/docs/markdown/Contributing.md b/docs/markdown/Contributing.md index d724b75..143cdcb 100644 --- a/docs/markdown/Contributing.md +++ b/docs/markdown/Contributing.md @@ -200,7 +200,7 @@ following: to avoid wasted effort Meson uses Flake8 for style guide enforcement. The Flake8 options for -the project are contained in setup.cfg. +the project are contained in .flake8. To run Flake8 on your local clone of Meson: diff --git a/docs/markdown/FAQ.md b/docs/markdown/FAQ.md index e5b7a9c..06379ae 100644 --- a/docs/markdown/FAQ.md +++ b/docs/markdown/FAQ.md @@ -402,3 +402,90 @@ the form `foo.lib` when building with MSVC, you can set the kwarg to `''` and the [`name_suffix:`](https://mesonbuild.com/Reference-manual.html#library) kwarg to `'lib'`. To get the default behaviour for each, you can either not specify the kwarg, or pass `[]` (an empty array) to it. + +## Do I need to add my headers to the sources list like in Autotools? + +Autotools requires you to add private and public headers to the sources list so +that it knows what files to include in the tarball generated by `make dist`. +Meson's `dist` command simply gathers everything committed to your git/hg +repository and adds it to the tarball, so adding headers to the sources list is +pointless. + +Meson uses Ninja which uses compiler dependency information to automatically +figure out dependencies between C sources and headers, so it will rebuild +things correctly when a header changes. + +The only exception to this are generated headers, for which you must [declare +dependencies correctly](#how-do-i-tell-meson-that-my-sources-use-generated-headers). + +If, for whatever reason, you do add non-generated headers to the sources list +of a target, Meson will simply ignore them. + +## How do I tell Meson that my sources use generated headers? + +Let's say you use a [`custom_target()`](https://mesonbuild.com/Reference-manual.html#custom_target) +to generate the headers, and then `#include` them in your C code. Here's how +you ensure that Meson generates the headers before trying to compile any +sources in the build target: + +```meson +libfoo_gen_headers = custom_target('gen-headers', ..., output: 'foo-gen.h') +libfoo_sources = files('foo-utils.c', 'foo-lib.c') +# Add generated headers to the list of sources for the build target +libfoo = library('foo', sources: libfoo_sources + libfoo_gen_headers) +``` + +Now let's say you have a new target that links to `libfoo`: + +```meson +libbar_sources = files('bar-lib.c') +libbar = library('bar', sources: libbar_sources, link_with: libfoo) +``` + +This adds a **link-time** dependency between the two targets, but note that the +sources of the targets have **no compile-time** dependencies and can be built +in any order; which improves parallelism and speeds up builds. + +If the sources in `libbar` *also* use `foo-gen.h`, that's a *compile-time* +dependency, and you'll have to add `libfoo_gen_headers` to `sources:` for +`libbar` too: + +```meson +libbar_sources = files('bar-lib.c') +libbar = library('bar', sources: libbar_sources + libfoo_gen_headers, link_with: libfoo) +``` + +Alternatively, if you have multiple libraries with sources that link to +a library and also use its generated headers, this code is equivalent to above: + +```meson +# Add generated headers to the list of sources for the build target +libfoo = library('foo', sources: libfoo_sources + libfoo_gen_headers) + +# Declare a dependency that will add the generated headers to sources +libfoo_dep = declare_dependency(link_with: libfoo, sources: libfoo_gen_headers) + +... + +libbar = library('bar', sources: libbar_sources, dependencies: libfoo_dep) +``` + +**Note:** You should only add *headers* to `sources:` while declaring +a dependency. If your custom target outputs both sources and headers, you can +use the subscript notation to get only the header(s): + +```meson +libfoo_gen_sources = custom_target('gen-headers', ..., output: ['foo-gen.h', 'foo-gen.c']) +libfoo_gen_headers = libfoo_gen_sources[0] + +# Add static and generated sources to the target +libfoo = library('foo', sources: libfoo_sources + libfoo_gen_sources) + +# Declare a dependency that will add the generated *headers* to sources +libfoo_dep = declare_dependency(link_with: libfoo, sources: libfoo_gen_headers) +... +libbar = library('bar', sources: libbar_sources, dependencies: libfoo_dep) +``` + +A good example of a generator that outputs both sources and headers is +[`gnome.mkenums()`](https://mesonbuild.com/Gnome-module.html#gnomemkenums). diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md index 3793ce3..195c451 100644 --- a/docs/markdown/Reference-manual.md +++ b/docs/markdown/Reference-manual.md @@ -119,6 +119,20 @@ Note that all these options are also available while running the `meson test` script for running tests instead of `ninja test` or `msbuild RUN_TESTS.vcxproj`, etc depending on the backend. +### alias_target + +``` meson +runtarget alias_target(target_name, dep1, ...) +``` + +Since *0.52.0* + +This function creates a new top-level target. Like all top-level targets, this +integrates with the selected backend. For instance, with Ninja you can +run it as `ninja target_name`. This is a dummy target that does not execute any +command, but ensures that all dependencies are built. Dependencies can be any +build target (e.g. return value of executable(), custom_target(), etc) + ### assert() ``` meson diff --git a/docs/markdown/snippets/alias_target.md b/docs/markdown/snippets/alias_target.md new file mode 100644 index 0000000..129730d --- /dev/null +++ b/docs/markdown/snippets/alias_target.md @@ -0,0 +1,12 @@ +## alias_target + +``` meson +runtarget alias_target(target_name, dep1, ...) +``` + +This function creates a new top-level target. Like all top-level targets, this +integrates with the selected backend. For instance, with Ninja you can +run it as `ninja target_name`. This is a dummy target that does not execute any +command, but ensures that all dependencies are built. Dependencies can be any +build target (e.g. return value of executable(), custom_target(), etc) + diff --git a/mesonbuild/ast/interpreter.py b/mesonbuild/ast/interpreter.py index 5354710..0e490ab 100644 --- a/mesonbuild/ast/interpreter.py +++ b/mesonbuild/ast/interpreter.py @@ -117,6 +117,7 @@ class AstInterpreter(interpreterbase.InterpreterBase): 'add_test_setup': self.func_do_nothing, 'find_library': self.func_do_nothing, 'subdir_done': self.func_do_nothing, + 'alias_target': self.func_do_nothing, }) def func_do_nothing(self, node, args, kwargs): diff --git a/mesonbuild/ast/printer.py b/mesonbuild/ast/printer.py index 2de1d0c..c6fb91a 100644 --- a/mesonbuild/ast/printer.py +++ b/mesonbuild/ast/printer.py @@ -118,6 +118,7 @@ class AstPrinter(AstVisitor): self.newline() def visit_IndexNode(self, node: mparser.IndexNode): + node.iobject.accept(self) self.append('[', node) node.index.accept(self) self.append(']', node) @@ -181,7 +182,7 @@ class AstPrinter(AstVisitor): def visit_ArgumentNode(self, node: mparser.ArgumentNode): break_args = (len(node.arguments) + len(node.kwargs)) > self.arg_newline_cutoff for i in node.arguments + list(node.kwargs.values()): - if not isinstance(i, mparser.ElementaryNode): + if not isinstance(i, (mparser.ElementaryNode, mparser.IndexNode)): break_args = True if break_args: self.newline() diff --git a/mesonbuild/ast/visitor.py b/mesonbuild/ast/visitor.py index fab4ed2..de13dae 100644 --- a/mesonbuild/ast/visitor.py +++ b/mesonbuild/ast/visitor.py @@ -84,6 +84,7 @@ class AstVisitor: def visit_IndexNode(self, node: mparser.IndexNode): self.visit_default_func(node) + node.iobject.accept(self) node.index.accept(self) def visit_MethodNode(self, node: mparser.MethodNode): diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index b830e37..b57a783 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -702,6 +702,13 @@ int dummy; self.add_build(elem) self.processed_targets[target.get_id()] = True + def build_run_target_name(self, target): + if target.subproject != '': + subproject_prefix = '{}@@'.format(target.subproject) + else: + subproject_prefix = '' + return '{}{}'.format(subproject_prefix, target.name) + def generate_run_target(self, target): cmd = self.environment.get_build_command() + ['--internal', 'commandrunner'] deps = self.unwrap_dep_list(target) @@ -718,12 +725,6 @@ int dummy; arg_strings.append(os.path.join(self.environment.get_build_dir(), relfname)) else: raise AssertionError('Unreachable code in generate_run_target: ' + str(i)) - if target.subproject != '': - subproject_prefix = '{}@@'.format(target.subproject) - else: - subproject_prefix = '' - target_name = 'meson-{}{}'.format(subproject_prefix, target.name) - elem = NinjaBuildElement(self.all_outputs, target_name, 'CUSTOM_COMMAND', []) cmd += [self.environment.get_source_dir(), self.environment.get_build_dir(), target.subdir] + self.environment.get_build_command() @@ -756,14 +757,21 @@ int dummy; cmd.append(target.command) cmd += arg_strings + if texe: + target_name = 'meson-{}'.format(self.build_run_target_name(target)) + elem = NinjaBuildElement(self.all_outputs, target_name, 'CUSTOM_COMMAND', []) + elem.add_item('COMMAND', cmd) + elem.add_item('description', 'Running external command %s.' % target.name) + elem.add_item('pool', 'console') + # Alias that runs the target defined above with the name the user specified + self.create_target_alias(target_name) + else: + target_name = self.build_run_target_name(target) + elem = NinjaBuildElement(self.all_outputs, target_name, 'phony', []) + elem.add_dep(deps) cmd = self.replace_paths(target, cmd) - elem.add_item('COMMAND', cmd) - elem.add_item('description', 'Running external command %s.' % target.name) - elem.add_item('pool', 'console') self.add_build(elem) - # Alias that runs the target defined above with the name the user specified - self.create_target_alias(target_name) self.processed_targets[target.get_id()] = True def generate_coverage_command(self, elem, outputs): diff --git a/mesonbuild/backend/vs2010backend.py b/mesonbuild/backend/vs2010backend.py index 86a7f83..82fc0cf 100644 --- a/mesonbuild/backend/vs2010backend.py +++ b/mesonbuild/backend/vs2010backend.py @@ -516,11 +516,16 @@ class Vs2010Backend(backends.Backend): def gen_run_target_vcxproj(self, target, ofname, guid): root = self.create_basic_crap(target, guid) - cmd_raw = [target.command] + target.args + if not target.command: + # FIXME: This is an alias target that doesn't run any command, there + # is probably a better way than running a this dummy command. + cmd_raw = python_command + ['-c', 'exit'] + else: + cmd_raw = [target.command] + target.args cmd = python_command + \ [os.path.join(self.environment.get_script_dir(), 'commandrunner.py'), - self.environment.get_build_dir(), self.environment.get_source_dir(), + self.environment.get_build_dir(), self.get_target_dir(target)] + self.environment.get_build_command() for i in cmd_raw: if isinstance(i, build.BuildTarget): diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 55b1629..2e23a59 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -2205,6 +2205,10 @@ class RunTarget(Target): def type_suffix(self): return "@run" +class AliasTarget(RunTarget): + def __init__(self, name, dependencies, subdir, subproject): + super().__init__(name, '', [], dependencies, subdir, subproject) + class Jar(BuildTarget): known_kwargs = known_jar_kwargs diff --git a/mesonbuild/cmake/data/run_ctgt.py b/mesonbuild/cmake/data/run_ctgt.py new file mode 100755 index 0000000..0a9b80d --- /dev/null +++ b/mesonbuild/cmake/data/run_ctgt.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +import argparse +import subprocess +import shutil +import os +import sys + +commands = [[]] +SEPERATOR = ';;;' + +# Generate CMD parameters +parser = argparse.ArgumentParser(description='Wrapper for add_custom_command') +parser.add_argument('-d', '--directory', type=str, metavar='D', required=True, help='Working directory to cwd to') +parser.add_argument('-o', '--outputs', nargs='+', metavar='O', required=True, help='Expected output files') +parser.add_argument('-O', '--original-outputs', nargs='+', metavar='O', required=True, help='Output files expected by CMake') +parser.add_argument('commands', nargs=argparse.REMAINDER, help='A "{}" seperated list of commands'.format(SEPERATOR)) + +# Parse +args = parser.parse_args() + +if len(args.outputs) != len(args.original_outputs): + print('Length of output list and original output list differ') + sys.exit(1) + +for i in args.commands: + if i == SEPERATOR: + commands += [[]] + continue + + commands[-1] += [i] + +# Execute +for i in commands: + # Skip empty lists + if not i: + continue + + subprocess.run(i, cwd=args.directory) + +# Copy outputs +zipped_outputs = zip(args.outputs, args.original_outputs) +for expected, generated in zipped_outputs: + do_copy = False + if not os.path.exists(expected): + if not os.path.exists(generated): + print('Unable to find generated file. This can cause the build to fail:') + print(generated) + do_copy = False + else: + do_copy = True + elif os.path.exists(generated): + if os.path.getmtime(generated) > os.path.getmtime(expected): + do_copy = True + + if do_copy: + if os.path.exists(expected): + os.remove(expected) + shutil.copyfile(generated, expected) diff --git a/mesonbuild/cmake/executor.py b/mesonbuild/cmake/executor.py index 13e31c1..fa09c4d 100644 --- a/mesonbuild/cmake/executor.py +++ b/mesonbuild/cmake/executor.py @@ -24,7 +24,7 @@ from typing import List, Tuple, Optional, TYPE_CHECKING if TYPE_CHECKING: from ..dependencies.base import ExternalProgram -import re, os, ctypes +import re, os, shutil, ctypes class CMakeExecutor: # The class's copy of the CMake path. Avoids having to search for it @@ -162,6 +162,41 @@ class CMakeExecutor: os.makedirs(build_dir, exist_ok=True) + # Try to set the correct compiler for C and C++ + # This step is required to make try_compile work inside CMake + fallback = os.path.realpath(__file__) # A file used as a fallback wehen everything else fails + compilers = self.environment.coredata.compilers[MachineChoice.BUILD] + + def make_abs(exe: str, lang: str): + if os.path.isabs(exe): + return exe + + p = shutil.which(exe) + if p is None: + mlog.debug('Failed to find a {} compiler for CMake. This might cause CMake to fail.'.format(lang)) + p = fallback + return p + + def choose_compiler(lang: str): + exe_list = [] + if lang in compilers: + exe_list = compilers[lang].get_exelist() + else: + comp_obj = self.environment.compiler_from_language(lang, MachineChoice.BUILD) + if comp_obj is not None: + exe_list = comp_obj.get_exelist() + + if len(exe_list) == 1: + return make_abs(exe_list[0], lang), '' + elif len(exe_list) == 2: + return make_abs(exe_list[1], lang), make_abs(exe_list[0], lang) + else: + mlog.debug('Failed to find a {} compiler for CMake. This might cause CMake to fail.'.format(lang)) + return fallback, '' + + c_comp, c_launcher = choose_compiler('c') + cxx_comp, cxx_launcher = choose_compiler('cpp') + # Reset the CMake cache with open('{}/CMakeCache.txt'.format(build_dir), 'w') as fp: fp.write('CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1\n') @@ -170,32 +205,38 @@ class CMakeExecutor: comp_dir = '{}/CMakeFiles/{}'.format(build_dir, self.cmakevers) os.makedirs(comp_dir, exist_ok=True) - c_comp = '{}/CMakeCCompiler.cmake'.format(comp_dir) - cxx_comp = '{}/CMakeCXXCompiler.cmake'.format(comp_dir) + c_comp_file = '{}/CMakeCCompiler.cmake'.format(comp_dir) + cxx_comp_file = '{}/CMakeCXXCompiler.cmake'.format(comp_dir) - if not os.path.exists(c_comp): - with open(c_comp, 'w') as fp: + if not os.path.exists(c_comp_file): + with open(c_comp_file, 'w') as fp: fp.write('''# Fake CMake file to skip the boring and slow stuff -set(CMAKE_C_COMPILER "{}") # Just give CMake a valid full path to any file +set(CMAKE_C_COMPILER "{}") # Should be a valid compiler for try_compile, etc. +set(CMAKE_C_COMPILER_LAUNCHER "{}") # The compiler launcher (if presentt) set(CMAKE_C_COMPILER_ID "GNU") # Pretend we have found GCC set(CMAKE_COMPILER_IS_GNUCC 1) set(CMAKE_C_COMPILER_LOADED 1) set(CMAKE_C_COMPILER_WORKS TRUE) set(CMAKE_C_ABI_COMPILED TRUE) +set(CMAKE_C_SOURCE_FILE_EXTENSIONS c;m) +set(CMAKE_C_IGNORE_EXTENSIONS h;H;o;O;obj;OBJ;def;DEF;rc;RC) set(CMAKE_SIZEOF_VOID_P "{}") -'''.format(os.path.realpath(__file__), ctypes.sizeof(ctypes.c_voidp))) +'''.format(c_comp, c_launcher, ctypes.sizeof(ctypes.c_voidp))) - if not os.path.exists(cxx_comp): - with open(cxx_comp, 'w') as fp: + if not os.path.exists(cxx_comp_file): + with open(cxx_comp_file, 'w') as fp: fp.write('''# Fake CMake file to skip the boring and slow stuff -set(CMAKE_CXX_COMPILER "{}") # Just give CMake a valid full path to any file +set(CMAKE_CXX_COMPILER "{}") # Should be a valid compiler for try_compile, etc. +set(CMAKE_CXX_COMPILER_LAUNCHER "{}") # The compiler launcher (if presentt) set(CMAKE_CXX_COMPILER_ID "GNU") # Pretend we have found GCC set(CMAKE_COMPILER_IS_GNUCXX 1) set(CMAKE_CXX_COMPILER_LOADED 1) set(CMAKE_CXX_COMPILER_WORKS TRUE) set(CMAKE_CXX_ABI_COMPILED TRUE) +set(CMAKE_CXX_IGNORE_EXTENSIONS inl;h;hpp;HPP;H;o;O;obj;OBJ;def;DEF;rc;RC) +set(CMAKE_CXX_SOURCE_FILE_EXTENSIONS C;M;c++;cc;cpp;cxx;mm;CPP) set(CMAKE_SIZEOF_VOID_P "{}") -'''.format(os.path.realpath(__file__), ctypes.sizeof(ctypes.c_voidp))) +'''.format(cxx_comp, cxx_launcher, ctypes.sizeof(ctypes.c_voidp))) return self.call(args, build_dir, env) diff --git a/mesonbuild/cmake/interpreter.py b/mesonbuild/cmake/interpreter.py index 88700f1..28a8488 100644 --- a/mesonbuild/cmake/interpreter.py +++ b/mesonbuild/cmake/interpreter.py @@ -18,15 +18,33 @@ from .common import CMakeException from .client import CMakeClient, RequestCMakeInputs, RequestConfigure, RequestCompute, RequestCodeModel, CMakeTarget from .executor import CMakeExecutor +from .traceparser import CMakeTraceParser, CMakeGeneratorTarget from .. import mlog from ..environment import Environment from ..mesonlib import MachineChoice -from ..mparser import Token, BaseNode, CodeBlockNode, FunctionNode, ArrayNode, ArgumentNode, AssignmentNode, BooleanNode, StringNode, IdNode, MethodNode -from ..compilers.compilers import lang_suffixes, header_suffixes, obj_suffixes -from subprocess import Popen, PIPE, STDOUT -from typing import List, Dict, Optional, TYPE_CHECKING +from ..compilers.compilers import lang_suffixes, header_suffixes, obj_suffixes, is_header +from subprocess import Popen, PIPE +from typing import Any, List, Dict, Optional, TYPE_CHECKING +from threading import Thread import os, re +from ..mparser import ( + Token, + BaseNode, + CodeBlockNode, + FunctionNode, + ArrayNode, + ArgumentNode, + AssignmentNode, + BooleanNode, + StringNode, + IdNode, + IndexNode, + MethodNode, + NumberNode, +) + + if TYPE_CHECKING: from ..build import Build from ..backend.backends import Backend @@ -87,6 +105,13 @@ blacklist_link_libs = [ 'advapi32.lib' ] +# Utility functions to generate local keys +def _target_key(tgt_name: str) -> str: + return '__tgt_{}__'.format(tgt_name) + +def _generated_file_key(fname: str) -> str: + return '__gen_{}__'.format(os.path.basename(fname)) + class ConverterTarget: lang_cmake_to_meson = {val.lower(): key for key, val in language_map.items()} @@ -184,11 +209,19 @@ class ConverterTarget: temp += [i] self.link_libraries = temp + # Filter out files that are not supported by the language + supported = list(header_suffixes) + list(obj_suffixes) + for i in self.languages: + supported += list(lang_suffixes[i]) + supported = ['.{}'.format(x) for x in supported] + self.sources = [x for x in self.sources if any([x.endswith(y) for y in supported])] + self.generated = [x for x in self.generated if any([x.endswith(y) for y in supported])] + # Make paths relative - def rel_path(x: str, is_header: bool) -> Optional[str]: + def rel_path(x: str, is_header: bool, is_generated: bool) -> Optional[str]: if not os.path.isabs(x): x = os.path.normpath(os.path.join(self.src_dir, x)) - if not os.path.exists(x) and not any([x.endswith(y) for y in obj_suffixes]): + if not os.path.exists(x) and not any([x.endswith(y) for y in obj_suffixes]) and not is_generated: mlog.warning('CMake: path', mlog.bold(x), 'does not exist. Ignoring. This can lead to build errors') return None if os.path.isabs(x) and os.path.commonpath([x, self.env.get_build_dir()]) == self.env.get_build_dir(): @@ -200,23 +233,29 @@ class ConverterTarget: return os.path.relpath(x, root_src_dir) return x + def custom_target(x: str): + key = _generated_file_key(x) + if key in output_target_map: + ctgt = output_target_map[key] + assert(isinstance(ctgt, ConverterCustomTarget)) + ref = ctgt.get_ref(x) + assert(isinstance(ref, CustomTargetReference) and ref.valid()) + return ref + 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, True) for x in set(self.includes)] + [build_dir_rel])) - self.sources = [rel_path(x, False) for x in self.sources] - self.generated = [rel_path(x, False) for x in self.generated] + self.includes = list(set([rel_path(x, True, False) for x in set(self.includes)] + [build_dir_rel])) + self.sources = [rel_path(x, False, False) for x in self.sources] + self.generated = [rel_path(x, False, True) for x in self.generated] + + # Resolve custom targets + self.generated = [custom_target(x) for x in self.generated] + # Remove delete entries self.includes = [x for x in self.includes if x is not None] self.sources = [x for x in self.sources if x is not None] self.generated = [x for x in self.generated if x is not None] - # Filter out files that are not supported by the language - supported = list(header_suffixes) + list(obj_suffixes) - for i in self.languages: - supported += list(lang_suffixes[i]) - supported = ['.{}'.format(x) for x in supported] - self.sources = [x for x in self.sources if any([x.endswith(y) for y in supported])] - self.generated = [x for x in self.generated if any([x.endswith(y) for y in supported])] - # Make sure '.' is always in the include directories if '.' not in self.includes: self.includes += ['.'] @@ -239,7 +278,8 @@ class ConverterTarget: 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 self.generated if isinstance(x, str)] + temp = [os.path.basename(x) for x in temp] 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 @@ -251,7 +291,7 @@ class ConverterTarget: 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])] + self.generated = [x for x in self.generated if not isinstance(x, str) or not any([x.endswith('.' + y) for y in obj_suffixes])] def meson_func(self) -> str: return target_type_map.get(self.type.upper()) @@ -277,6 +317,113 @@ class ConverterTarget: for key, val in self.compile_opts.items(): mlog.log(' -', key, '=', mlog.bold(str(val))) +class CustomTargetReference: + def __init__(self, ctgt: 'ConverterCustomTarget', index: int): + self.ctgt = ctgt # type: ConverterCustomTarget + self.index = index # type: int + + def __repr__(self) -> str: + if self.valid(): + return '<{}: {} [{}]>'.format(self.__class__.__name__, self.ctgt.name, self.ctgt.outputs[self.index]) + else: + return '<{}: INVALID REFERENCE>'.format(self.__class__.__name__) + + def valid(self) -> bool: + return self.ctgt is not None and self.index >= 0 + + def filename(self) -> str: + return self.ctgt.outputs[self.index] + +class ConverterCustomTarget: + tgt_counter = 0 # type: int + + def __init__(self, target: CMakeGeneratorTarget): + self.name = 'custom_tgt_{}'.format(ConverterCustomTarget.tgt_counter) + self.original_outputs = list(target.outputs) + self.outputs = [os.path.basename(x) for x in self.original_outputs] + self.command = target.command + self.working_dir = target.working_dir + self.depends_raw = target.depends + self.inputs = [] + self.depends = [] + + ConverterCustomTarget.tgt_counter += 1 + + def __repr__(self) -> str: + return '<{}: {}>'.format(self.__class__.__name__, self.outputs) + + def postprocess(self, output_target_map: dict, root_src_dir: str, subdir: str, build_dir: str) -> None: + # Default the working directory to the CMake build dir. This + # is not 100% correct, since it should be the value of + # ${CMAKE_CURRENT_BINARY_DIR} when add_custom_command is + # called. However, keeping track of this variable is not + # trivial and the current solution should work in most cases. + if not self.working_dir: + self.working_dir = build_dir + + # relative paths in the working directory are always relative + # to ${CMAKE_CURRENT_BINARY_DIR} (see note above) + if not os.path.isabs(self.working_dir): + self.working_dir = os.path.normpath(os.path.join(build_dir, self.working_dir)) + + # Modify the original outputs if they are relative. Again, + # relative paths are relative to ${CMAKE_CURRENT_BINARY_DIR} + # and the first disclaimer is stil in effect + def ensure_absolute(x: str): + if os.path.isabs(x): + return x + else: + return os.path.normpath(os.path.join(build_dir, x)) + self.original_outputs = [ensure_absolute(x) for x in self.original_outputs] + + # Check if the command is a build target + commands = [] + for i in self.command: + assert(isinstance(i, list)) + cmd = [] + + for j in i: + target_key = _target_key(j) + if target_key in output_target_map: + cmd += [output_target_map[target_key]] + else: + cmd += [j] + + commands += [cmd] + self.command = commands + + # Check dependencies and input files + for i in self.depends_raw: + tgt_key = _target_key(i) + gen_key = _generated_file_key(i) + + if os.path.basename(i) in output_target_map: + self.depends += [output_target_map[os.path.basename(i)]] + elif tgt_key in output_target_map: + self.depends += [output_target_map[tgt_key]] + elif gen_key in output_target_map: + self.inputs += [output_target_map[gen_key].get_ref(i)] + elif not os.path.isabs(i) and os.path.exists(os.path.join(root_src_dir, i)): + self.inputs += [i] + elif os.path.isabs(i) and os.path.exists(i) and os.path.commonpath([i, root_src_dir]) == root_src_dir: + self.inputs += [os.path.relpath(i, root_src_dir)] + + def get_ref(self, fname: str) -> Optional[CustomTargetReference]: + try: + idx = self.outputs.index(os.path.basename(fname)) + return CustomTargetReference(self, idx) + except ValueError: + return None + + def log(self) -> None: + mlog.log('Custom Target', mlog.bold(self.name)) + mlog.log(' -- command: ', mlog.bold(str(self.command))) + mlog.log(' -- outputs: ', mlog.bold(str(self.outputs))) + mlog.log(' -- working_dir: ', mlog.bold(str(self.working_dir))) + mlog.log(' -- depends_raw: ', mlog.bold(str(self.depends_raw))) + mlog.log(' -- inputs: ', mlog.bold(str(self.inputs))) + mlog.log(' -- depends: ', mlog.bold(str(self.depends))) + class CMakeInterpreter: def __init__(self, build: 'Build', subdir: str, src_dir: str, install_prefix: str, env: Environment, backend: 'Backend'): assert(hasattr(backend, 'name')) @@ -293,11 +440,14 @@ class CMakeInterpreter: # Raw CMake results self.bs_files = [] self.codemodel = None + self.raw_trace = None # Analysed data self.project_name = '' self.languages = [] self.targets = [] + self.custom_targets = [] # type: List[ConverterCustomTarget] + self.trace = CMakeTraceParser() # Generated meson data self.generated_targets = {} @@ -327,6 +477,7 @@ class CMakeInterpreter: 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 += ['--trace', '--trace-expand'] cmake_args += extra_cmake_options # Run CMake @@ -338,17 +489,25 @@ class CMakeInterpreter: 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) + proc = Popen(cmake_args + [self.src_dir], stdout=PIPE, stderr=PIPE, 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')) + def print_stdout(): + while True: + line = proc.stdout.readline() + if not line: + break + mlog.log(line.decode('utf-8').strip('\n')) + proc.stdout.close() - # Wait for CMake to finish - proc.communicate() + t = Thread(target=print_stdout) + t.start() + + self.raw_trace = proc.stderr.read() + self.raw_trace = self.raw_trace.decode('utf-8') + proc.stderr.close() + proc.wait() + + t.join() mlog.log() h = mlog.green('SUCCEEDED') if proc.returncode == 0 else mlog.red('FAILED') @@ -391,6 +550,11 @@ class CMakeInterpreter: self.project_name = '' self.languages = [] self.targets = [] + self.custom_targets = [] + self.trace = CMakeTraceParser(permissive=True) + + # Parse the trace + self.trace.parse(self.raw_trace) # Find all targets for i in self.codemodel.configs: @@ -401,13 +565,24 @@ class CMakeInterpreter: 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.trace.custom_targets: + self.custom_targets += [ConverterCustomTarget(i)] + + # generate the output_target_map + output_target_map = {} + output_target_map.update({x.full_name: x for x in self.targets}) + output_target_map.update({_target_key(x.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 + for i in self.custom_targets: + for j in i.original_outputs: + output_target_map[_generated_file_key(j)] = i object_libs = [] # First pass: Basic target cleanup + for i in self.custom_targets: + i.postprocess(output_target_map, self.src_dir, self.subdir, self.build_dir) for i in self.targets: i.postprocess(output_target_map, self.src_dir, self.subdir, self.install_prefix) if i.type == 'OBJECT_LIBRARY': @@ -418,7 +593,7 @@ class CMakeInterpreter: 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.') + 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: if not self.project_name: @@ -433,15 +608,23 @@ class CMakeInterpreter: def id_node(value: str) -> IdNode: return IdNode(token(val=value)) + def number(value: int) -> NumberNode: + return NumberNode(token(val=value)) + def nodeify(value): if isinstance(value, str): return string(value) elif isinstance(value, bool): return BooleanNode(token(), value) + elif isinstance(value, int): + return number(value) elif isinstance(value, list): return array(value) return value + def indexed(node: BaseNode, index: int) -> IndexNode: + return IndexNode(node, nodeify(index)) + def array(elements) -> ArrayNode: args = ArgumentNode(token()) if not isinstance(elements, list): @@ -480,12 +663,30 @@ class CMakeInterpreter: # Generate the root code block and the project function call root_cb = CodeBlockNode(token()) root_cb.lines += [function('project', [self.project_name] + self.languages)] + + # Add the run script for custom commands + run_script = '{}/data/run_ctgt.py'.format(os.path.dirname(os.path.realpath(__file__))) + run_script_var = 'ctgt_run_script' + root_cb.lines += [assign(run_script_var, function('find_program', [[run_script]], {'required': True}))] + + # Add the targets processed = {} + def resolve_ctgt_ref(ref: CustomTargetReference) -> BaseNode: + tgt_var = processed[ref.ctgt.name]['tgt'] + if len(ref.ctgt.outputs) == 1: + return id_node(tgt_var) + else: + return indexed(id_node(tgt_var), ref.index) + def process_target(tgt: ConverterTarget): # First handle inter target dependencies link_with = [] objec_libs = [] + sources = [] + generated = [] + generated_filenames = [] + custom_targets = [] for i in tgt.link_with: assert(isinstance(i, ConverterTarget)) if i.name not in processed: @@ -497,6 +698,32 @@ class CMakeInterpreter: process_target(i) objec_libs += [processed[i.name]['tgt']] + # Generate the source list and handle generated sources + for i in tgt.sources + tgt.generated: + if isinstance(i, CustomTargetReference): + if i.ctgt.name not in processed: + process_custom_target(i.ctgt) + generated += [resolve_ctgt_ref(i)] + generated_filenames += [i.filename()] + if i.ctgt not in custom_targets: + custom_targets += [i.ctgt] + else: + sources += [i] + + # Add all header files from all used custom targets. This + # ensures that all custom targets are built before any + # sources of the current target are compiled and thus all + # header files are present. This step is necessary because + # CMake always ensures that a custom target is executed + # before another target if at least one output is used. + for i in custom_targets: + for j in i.outputs: + if not is_header(j) or j in generated_filenames: + continue + + generated += [resolve_ctgt_ref(i.get_ref(j))] + generated_filenames += [j] + # Determine the meson function to use for the build target tgt_func = tgt.meson_func() if not tgt_func: @@ -540,15 +767,59 @@ class CMakeInterpreter: # 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)) + src_node = assign(src_var, function('files', sources)) + tgt_node = assign(tgt_var, function(tgt_func, [base_name, [id_node(src_var)] + generated], 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} + def process_custom_target(tgt: ConverterCustomTarget) -> None: + # CMake allows to specify multiple commands in a custom target. + # To map this to meson, a helper script is used to execute all + # commands in order. This addtionally allows setting the working + # directory. + + tgt_var = tgt.name # type: str + + def resolve_source(x: Any) -> Any: + if isinstance(x, ConverterTarget): + if x.name not in processed: + process_target(x) + return id_node(x.name) + elif isinstance(x, CustomTargetReference): + if x.ctgt.name not in processed: + process_custom_target(x.ctgt) + return resolve_ctgt_ref(x) + else: + return x + + # Generate the command list + command = [] + command += [id_node(run_script_var)] + command += ['-o', '@OUTPUT@'] + command += ['-O'] + tgt.original_outputs + command += ['-d', tgt.working_dir] + + # Generate the commands. Subcommands are seperated by ';;;' + for cmd in tgt.command: + command += [resolve_source(x) for x in cmd] + [';;;'] + + tgt_kwargs = { + 'input': [resolve_source(x) for x in tgt.inputs], + 'output': tgt.outputs, + 'command': command, + 'depends': [resolve_source(x) for x in tgt.depends], + } + + root_cb.lines += [assign(tgt_var, function('custom_target', [tgt.name], tgt_kwargs))] + processed[tgt.name] = {'inc': None, 'src': None, 'dep': None, 'tgt': tgt_var, 'func': 'custom_target'} + # Now generate the target function calls + for i in self.custom_targets: + if i.name not in processed: + process_custom_target(i) for i in self.targets: if i.name not in processed: process_target(i) diff --git a/mesonbuild/cmake/traceparser.py b/mesonbuild/cmake/traceparser.py index 1dcf6d2..4b87319 100644 --- a/mesonbuild/cmake/traceparser.py +++ b/mesonbuild/cmake/traceparser.py @@ -16,9 +16,11 @@ # or an interpreter-based tool. from .common import CMakeException +from .. import mlog -from typing import List, Tuple +from typing import List, Tuple, Optional import re +import os class CMakeTraceLine: def __init__(self, file, line, func, args): @@ -46,14 +48,26 @@ class CMakeTarget: propSTR += " '{}': {}\n".format(i, self.properies[i]) return s.format(self.name, self.type, propSTR) -class CMakeTraceParser: +class CMakeGeneratorTarget: def __init__(self): + self.outputs = [] # type: List[str] + self.command = [] # type: List[List[str]] + self.working_dir = None # type: Optional[str] + self.depends = [] # type: List[str] + +class CMakeTraceParser: + def __init__(self, permissive: bool = False): # Dict of CMake variables: '<var_name>': ['list', 'of', 'values'] self.vars = {} # Dict of CMakeTarget self.targets = {} + # List of targes that were added with add_custom_command to generate files + self.custom_targets = [] # type: List[CMakeGeneratorTarget] + + self.permissive = permissive # type: bool + def parse(self, trace: str) -> None: # First parse the trace lexer1 = self._lex_trace(trace) @@ -64,6 +78,7 @@ class CMakeTraceParser: 'unset': self._cmake_unset, 'add_executable': self._cmake_add_executable, 'add_library': self._cmake_add_library, + 'add_custom_command': self._cmake_add_custom_command, 'add_custom_target': self._cmake_add_custom_target, 'set_property': self._cmake_set_property, 'set_target_properties': self._cmake_set_target_properties @@ -102,6 +117,14 @@ class CMakeTraceParser: return True return False + def _gen_exception(self, function: str, error: str, tline: CMakeTraceLine) -> None: + # Generate an exception if the parser is not in permissive mode + + if self.permissive: + mlog.debug('CMake trace warning: {}() {}\n{}'.format(function, error, tline)) + return None + raise CMakeException('CMake: {}() {}\n{}'.format(function, error, tline)) + def _cmake_set(self, tline: CMakeTraceLine) -> None: """Handler for the CMake set() function in all variaties. @@ -132,7 +155,7 @@ class CMakeTraceParser: args.append(i) if len(args) < 1: - raise CMakeException('CMake: set() requires at least one argument\n{}'.format(tline)) + return self._gen_exception('set', 'requires at least one argument', tline) # Now that we've removed extra arguments all that should be left is the # variable identifier and the value, join the value back together to @@ -151,7 +174,7 @@ class CMakeTraceParser: def _cmake_unset(self, tline: CMakeTraceLine): # DOC: https://cmake.org/cmake/help/latest/command/unset.html if len(tline.args) < 1: - raise CMakeException('CMake: unset() requires at least one argument\n{}'.format(tline)) + return self._gen_exception('unset', 'requires at least one argument', tline) if tline.args[0] in self.vars: del self.vars[tline.args[0]] @@ -162,12 +185,12 @@ class CMakeTraceParser: # Make sure the exe is imported if 'IMPORTED' not in args: - raise CMakeException('CMake: add_executable() non imported executables are not supported\n{}'.format(tline)) + return self._gen_exception('add_executable', 'non imported executables are not supported', tline) args.remove('IMPORTED') if len(args) < 1: - raise CMakeException('CMake: add_executable() requires at least 1 argument\n{}'.format(tline)) + return self._gen_exception('add_executable', 'requires at least 1 argument', tline) self.targets[args[0]] = CMakeTarget(args[0], 'EXECUTABLE', {}) @@ -177,21 +200,82 @@ class CMakeTraceParser: # Make sure the lib is imported if 'IMPORTED' not in args: - raise CMakeException('CMake: add_library() non imported libraries are not supported\n{}'.format(tline)) + return self._gen_exception('add_library', 'non imported libraries are not supported', tline) args.remove('IMPORTED') # No only look at the first two arguments (target_name and target_type) and ignore the rest if len(args) < 2: - raise CMakeException('CMake: add_library() requires at least 2 arguments\n{}'.format(tline)) + return self._gen_exception('add_library', 'requires at least 2 arguments', tline) self.targets[args[0]] = CMakeTarget(args[0], args[1], {}) + def _cmake_add_custom_command(self, tline: CMakeTraceLine): + # DOC: https://cmake.org/cmake/help/latest/command/add_custom_command.html + args = list(tline.args) # Make a working copy + + if not args: + return self._gen_exception('add_custom_command', 'requires at least 1 argument', tline) + + # Skip the second function signature + if args[0] == 'TARGET': + return self._gen_exception('add_custom_command', 'TARGET syntax is currently not supported', tline) + + magic_keys = ['OUTPUT', 'COMMAND', 'MAIN_DEPENDENCY', 'DEPENDS', 'BYPRODUCTS', + 'IMPLICIT_DEPENDS', 'WORKING_DIRECTORY', 'COMMENT', 'DEPFILE', + 'JOB_POOL', 'VERBATIM', 'APPEND', 'USES_TERMINAL', 'COMMAND_EXPAND_LISTS'] + + target = CMakeGeneratorTarget() + + def handle_output(key: str, target: CMakeGeneratorTarget) -> None: + target.outputs += [key] + + def handle_command(key: str, target: CMakeGeneratorTarget) -> None: + if key == 'ARGS': + return + target.command[-1] += [key] + + def handle_depends(key: str, target: CMakeGeneratorTarget) -> None: + target.depends += [key] + + def handle_working_dir(key: str, target: CMakeGeneratorTarget) -> None: + if target.working_dir is None: + target.working_dir = key + else: + target.working_dir += ' ' + target.working_dir += key + + fn = None + + for i in args: + if i in magic_keys: + if i == 'OUTPUT': + fn = handle_output + elif i == 'DEPENDS': + fn = handle_depends + elif i == 'WORKING_DIRECTORY': + fn = handle_working_dir + elif i == 'COMMAND': + fn = handle_command + target.command += [[]] + else: + fn = None + continue + + if fn is not None: + fn(i, target) + + target.outputs = self._guess_files(target.outputs) + target.depends = self._guess_files(target.depends) + target.command = [self._guess_files(x) for x in target.command] + + self.custom_targets += [target] + def _cmake_add_custom_target(self, tline: CMakeTraceLine): # DOC: https://cmake.org/cmake/help/latest/command/add_custom_target.html # We only the first parameter (the target name) is interesting if len(tline.args) < 1: - raise CMakeException('CMake: add_custom_target() requires at least one argument\n{}'.format(tline)) + return self._gen_exception('add_custom_target', 'requires at least one argument', tline) self.targets[tline.args[0]] = CMakeTarget(tline.args[0], 'CUSTOM', {}) @@ -219,7 +303,7 @@ class CMakeTraceParser: targets.append(curr) if not args: - raise CMakeException('CMake: set_property() faild to parse argument list\n{}'.format(tline)) + return self._gen_exception('set_property', 'faild to parse argument list', tline) if len(args) == 1: # Tries to set property to nothing so nothing has to be done @@ -232,7 +316,7 @@ class CMakeTraceParser: for i in targets: if i not in self.targets: - raise CMakeException('CMake: set_property() TARGET {} not found\n{}'.format(i, tline)) + return self._gen_exception('set_property', 'TARGET {} not found'.format(i), tline) if identifier not in self.targets[i].properies: self.targets[i].properies[identifier] = [] @@ -284,7 +368,7 @@ class CMakeTraceParser: for name, value in arglist: for i in targets: if i not in self.targets: - raise CMakeException('CMake: set_target_properties() TARGET {} not found\n{}'.format(i, tline)) + return self._gen_exception('set_target_properties', 'TARGET {} not found'.format(i), tline) self.targets[i].properies[name] = value @@ -315,3 +399,35 @@ class CMakeTraceParser: args = list(map(lambda x: reg_genexp.sub('', x), args)) # Remove generator expressions yield CMakeTraceLine(file, line, func, args) + + def _guess_files(self, broken_list: List[str]) -> List[str]: + #Try joining file paths that contain spaces + + reg_start = re.compile(r'^([A-Za-z]:)?/.*/[^./]+$') + reg_end = re.compile(r'^.*\.[a-zA-Z]+$') + + fixed_list = [] # type: List[str] + curr_str = None # type: Optional[str] + + for i in broken_list: + if curr_str is None: + curr_str = i + elif os.path.isfile(curr_str): + # Abort concatination if curr_str is an existing file + fixed_list += [curr_str] + curr_str = i + elif not reg_start.match(curr_str): + # Abort concatination if curr_str no longer matches the regex + fixed_list += [curr_str] + curr_str = i + elif reg_end.match(i): + # File detected + curr_str = '{} {}'.format(curr_str, i) + fixed_list += [curr_str] + curr_str = None + else: + curr_str = '{} {}'.format(curr_str, i) + + if curr_str: + fixed_list += [curr_str] + return fixed_list diff --git a/mesonbuild/compilers/clike.py b/mesonbuild/compilers/clike.py index 6d540e7..16783fa 100644 --- a/mesonbuild/compilers/clike.py +++ b/mesonbuild/compilers/clike.py @@ -255,7 +255,8 @@ class CLikeCompiler: # a ton of compiler flags to differentiate between # arm and x86_64. So just compile. mode = 'compile' - extra_flags = self._get_basic_compiler_args(environment, mode) + cargs, largs = self._get_basic_compiler_args(environment, mode) + extra_flags = cargs + self.linker_to_compiler_args(largs) # Is a valid executable output for all toolchains and platforms binname += '.exe' @@ -264,7 +265,9 @@ class CLikeCompiler: with open(source_name, 'w') as ofile: ofile.write(code) # Compile sanity check - cmdlist = self.exelist + extra_flags + [source_name] + self.get_output_args(binary_name) + # NOTE: extra_flags must be added at the end. On MSVC, it might contain a '/link' argument + # after which all further arguments will be passed directly to the linker + cmdlist = self.exelist + [source_name] + self.get_output_args(binary_name) + extra_flags pc, stdo, stde = mesonlib.Popen_safe(cmdlist, cwd=work_dir) mlog.debug('Sanity check compiler command line:', ' '.join(cmdlist)) mlog.debug('Sanity check compile stdout:') @@ -329,10 +332,10 @@ class CLikeCompiler: dependencies=dependencies) def _get_basic_compiler_args(self, env, mode): - args = [] + cargs, largs = [], [] # Select a CRT if needed since we're linking if mode == 'link': - args += self.get_linker_debug_crt_args() + cargs += self.get_linker_debug_crt_args() # Add CFLAGS/CXXFLAGS/OBJCFLAGS/OBJCXXFLAGS and CPPFLAGS from the env sys_args = env.coredata.get_external_args(self.for_machine, self.language) @@ -341,17 +344,17 @@ class CLikeCompiler: # also used during linking. These flags can break # argument checks. Thanks, Autotools. cleaned_sys_args = self.remove_linkerlike_args(sys_args) - args += cleaned_sys_args + cargs += cleaned_sys_args if mode == 'link': # Add LDFLAGS from the env sys_ld_args = env.coredata.get_external_link_args(self.for_machine, self.language) # CFLAGS and CXXFLAGS go to both linking and compiling, but we want them # to only appear on the command line once. Remove dupes. - args += [x for x in sys_ld_args if x not in sys_args] + largs += [x for x in sys_ld_args if x not in sys_args] - args += self.get_compiler_args_for_mode(mode) - return args + cargs += self.get_compiler_args_for_mode(mode) + return cargs, largs def _get_compiler_check_args(self, env, extra_args, dependencies, mode='compile'): if extra_args is None: @@ -365,19 +368,27 @@ class CLikeCompiler: elif not isinstance(dependencies, list): dependencies = [dependencies] # Collect compiler arguments - args = compilers.CompilerArgs(self) + cargs = compilers.CompilerArgs(self) + largs = [] for d in dependencies: # Add compile flags needed by dependencies - args += d.get_compile_args() + cargs += d.get_compile_args() if mode == 'link': # Add link flags needed to find dependencies - args += d.get_link_args() + largs += d.get_link_args() + + ca, la = self._get_basic_compiler_args(env, mode) + cargs += ca + largs += la - args += self._get_basic_compiler_args(env, mode) + cargs += self.get_compiler_check_args() - args += self.get_compiler_check_args() - # extra_args must override all other arguments, so we add them last - args += extra_args + # on MSVC compiler and linker flags must be separated by the "/link" argument + # at this point, the '/link' argument may already be part of extra_args, otherwise, it is added here + if self.linker_to_compiler_args([]) == ['/link'] and largs != [] and not ('/link' in extra_args): + extra_args += ['/link'] + + args = cargs + extra_args + largs return args def compiles(self, code, env, *, extra_args=None, dependencies=None, mode='compile', disable_cache=False): @@ -387,8 +398,8 @@ class CLikeCompiler: def _build_wrapper(self, code, env, extra_args, dependencies=None, mode='compile', want_output=False, disable_cache=False, temp_dir=None): args = self._get_compiler_check_args(env, extra_args, dependencies, mode) if disable_cache or want_output: - return self.compile(code, extra_args=args, mode=mode, want_output=want_output, temp_dir=temp_dir) - return self.cached_compile(code, env.coredata, extra_args=args, mode=mode, temp_dir=temp_dir) + return self.compile(code, extra_args=args, mode=mode, want_output=want_output, temp_dir=env.scratch_dir) + return self.cached_compile(code, env.coredata, extra_args=args, mode=mode, temp_dir=env.scratch_dir) def links(self, code, env, *, extra_args=None, dependencies=None, disable_cache=False): return self.compiles(code, env, extra_args=extra_args, @@ -964,10 +975,12 @@ class CLikeCompiler: # search for .a. This is only allowed if libtype is LibType.PREFER_SHARED if ((not extra_dirs and libtype is LibType.PREFER_SHARED) or libname in self.internal_libs): - args = ['-l' + libname] - largs = self.linker_to_compiler_args(self.get_allow_undefined_link_args()) - if self.links(code, env, extra_args=(args + largs), disable_cache=True)[0]: - return args + cargs = ['-l' + libname] + largs = self.get_allow_undefined_link_args() + extra_args = cargs + self.linker_to_compiler_args(largs) + + if self.links(code, env, extra_args=extra_args, disable_cache=True)[0]: + return cargs # Don't do a manual search for internal libs if libname in self.internal_libs: return None diff --git a/mesonbuild/compilers/d.py b/mesonbuild/compilers/d.py index b7bc49a..9d9863b 100644 --- a/mesonbuild/compilers/d.py +++ b/mesonbuild/compilers/d.py @@ -356,6 +356,10 @@ class DCompiler(Compiler): # Translate common D arguments here. if arg == '-pthread': continue + if arg.startswith('-fstack-protector'): + continue + if arg.startswith('-D'): + continue if arg.startswith('-Wl,'): # Translate linker arguments here. linkargs = arg[arg.index(',') + 1:].split(',') diff --git a/mesonbuild/dependencies/base.py b/mesonbuild/dependencies/base.py index fdfd143..8616367 100644 --- a/mesonbuild/dependencies/base.py +++ b/mesonbuild/dependencies/base.py @@ -1357,7 +1357,11 @@ class CMakeDependency(ExternalDependency): if 'RELEASE' in cfgs: cfg = 'RELEASE' - if 'IMPORTED_LOCATION_{}'.format(cfg) in tgt.properies: + if 'IMPORTED_IMPLIB_{}'.format(cfg) in tgt.properies: + libraries += tgt.properies['IMPORTED_IMPLIB_{}'.format(cfg)] + elif 'IMPORTED_IMPLIB' in tgt.properies: + libraries += tgt.properies['IMPORTED_IMPLIB'] + elif 'IMPORTED_LOCATION_{}'.format(cfg) in tgt.properies: libraries += tgt.properies['IMPORTED_LOCATION_{}'.format(cfg)] elif 'IMPORTED_LOCATION' in tgt.properies: libraries += tgt.properies['IMPORTED_LOCATION'] diff --git a/mesonbuild/dependencies/boost.py b/mesonbuild/dependencies/boost.py index 5c9e0b5..340a5a9 100644 --- a/mesonbuild/dependencies/boost.py +++ b/mesonbuild/dependencies/boost.py @@ -134,22 +134,31 @@ class BoostDependency(ExternalDependency): else: self.incdir = self.detect_nix_incdir() - if self.check_invalid_modules(): - return - mlog.debug('Boost library root dir is', mlog.bold(self.boost_root)) mlog.debug('Boost include directory is', mlog.bold(self.incdir)) # 1. check if we can find BOOST headers. self.detect_headers_and_version() + if not self.is_found: + return # if we can not find 'boost/version.hpp' + # 2. check if we can find BOOST libraries. - if self.is_found: - self.detect_lib_modules() - mlog.debug('Boost library directory is', mlog.bold(self.libdir)) + self.detect_lib_modules() + mlog.debug('Boost library directory is', mlog.bold(self.libdir)) + + mlog.debug('Installed Boost libraries: ') + for key in sorted(self.lib_modules.keys()): + mlog.debug(key, self.lib_modules[key]) + + # 3. check if requested modules are valid, that is, either found or in the list of known boost libraries + self.check_invalid_modules() + + # 4. final check whether or not we find all requested and valid modules + self.check_find_requested_modules() def check_invalid_modules(self): - invalid_modules = [c for c in self.requested_modules if 'boost_' + c not in BOOST_LIBS] + invalid_modules = [c for c in self.requested_modules if 'boost_' + c not in self.lib_modules and 'boost_' + c not in BOOST_LIBS] # previous versions of meson allowed include dirs as modules remove = [] @@ -273,6 +282,7 @@ class BoostDependency(ExternalDependency): else: self.detect_lib_modules_nix() + def check_find_requested_modules(self): # 3. Check if we can find the modules for m in self.requested_modules: if 'boost_' + m not in self.lib_modules: @@ -491,7 +501,6 @@ class BoostDependency(ExternalDependency): def get_sources(self): return [] - # Generated with boost_names.py BOOST_LIBS = [ 'boost_atomic', @@ -547,10 +556,6 @@ BOOST_LIBS = [ 'boost_math_c99l', 'boost_mpi', 'boost_program_options', - 'boost_python', - 'boost_python3', - 'boost_numpy', - 'boost_numpy3', 'boost_random', 'boost_regex', 'boost_serialization', diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index f5bb4e5..fd94251 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -863,10 +863,9 @@ class CustomTargetHolder(TargetHolder): return IncludeDirsHolder(build.IncludeDirs('', [], False, [os.path.join('@BUILD_ROOT@', self.interpreter.backend.get_target_dir(self.held_object))])) -class RunTargetHolder(InterpreterObject, ObjectHolder): - def __init__(self, name, command, args, dependencies, subdir, subproject): - InterpreterObject.__init__(self) - ObjectHolder.__init__(self, build.RunTarget(name, command, args, dependencies, subdir, subproject)) +class RunTargetHolder(TargetHolder): + def __init__(self, target, interp): + super().__init__(target, interp) def __repr__(self): r = '<{} {}: {}>' @@ -2103,6 +2102,7 @@ class Interpreter(InterpreterBase): 'add_project_link_arguments': self.func_add_project_link_arguments, 'add_test_setup': self.func_add_test_setup, 'add_languages': self.func_add_languages, + 'alias_target': self.func_alias_target, 'assert': self.func_assert, 'benchmark': self.func_benchmark, 'build_target': self.func_build_target, @@ -3300,7 +3300,23 @@ This will become a hard error in the future.''' % kwargs['input'], location=self raise InterpreterException('Depends items must be build targets.') cleaned_deps.append(d) command, *cmd_args = cleaned_args - tg = RunTargetHolder(name, command, cmd_args, cleaned_deps, self.subdir, self.subproject) + tg = RunTargetHolder(build.RunTarget(name, command, cmd_args, cleaned_deps, self.subdir, self.subproject), self) + self.add_target(name, tg.held_object) + return tg + + @FeatureNew('alias_target', '0.52.0') + @noKwargs + def func_alias_target(self, node, args, kwargs): + if len(args) < 2: + raise InvalidCode('alias_target takes at least 2 arguments.') + name = args[0] + if not isinstance(name, str): + raise InterpreterException('First argument must be a string.') + deps = listify(args[1:], unholder=True) + for d in deps: + if not isinstance(d, (build.BuildTarget, build.CustomTarget)): + raise InterpreterException('Depends items must be build targets.') + tg = RunTargetHolder(build.AliasTarget(name, deps, self.subdir, self.subproject), self) self.add_target(name, tg.held_object) return tg @@ -3853,6 +3869,23 @@ different subdirectory. for_machine = self.machine_from_native_kwarg(kwargs) self.add_project_arguments(node, self.build.projects_link_args[for_machine], args, kwargs) + def warn_about_builtin_args(self, args): + warnargs = ('/W1', '/W2', '/W3', '/W4', '/Wall', '-Wall', '-Wextra', '-Wpedantic') + optargs = ('-O0', '-O2', '-O3', '-Os', '/O1', '/O2', '/Os') + for arg in args: + if arg in warnargs: + mlog.warning("Consider using the builtin warning_level option instead of adding warning flags by hand.") + elif arg in optargs: + mlog.warning('Consider using the builtin optimization level rather than adding flags by hand.') + elif arg == '-g': + mlog.warning('Consider using the builtin debug option rather than adding flags by hand.') + elif arg == '-pipe': + mlog.warning("You don't need to add -pipe, Meson will use it automatically when it is available.") + elif arg.startswith('-fsanitize'): + mlog.warning('Consider using the builtin option for sanitizers rather than adding flags by hand.') + elif arg.startswith('-std=') or arg.startswith('/std:'): + mlog.warning('Consider using the builtin option for language standard version rather than adding flags by hand.') + def add_global_arguments(self, node, argsdict, args, kwargs): if self.is_subproject(): msg = 'Function \'{}\' cannot be used in subprojects because ' \ @@ -3881,6 +3914,8 @@ different subdirectory. if 'language' not in kwargs: raise InvalidCode('Missing language definition in {}'.format(node.func_name)) + self.warn_about_builtin_args(args) + for lang in mesonlib.stringlistify(kwargs['language']): lang = lang.lower() argsdict[lang] = argsdict.get(lang, []) + args diff --git a/mesonbuild/mlog.py b/mesonbuild/mlog.py index 79dee47..d13defb 100644 --- a/mesonbuild/mlog.py +++ b/mesonbuild/mlog.py @@ -40,10 +40,13 @@ def _windows_ansi() -> bool: # original behavior return bool(kernel.SetConsoleMode(stdout, mode.value | 0x4) or os.environ.get('ANSICON')) -if platform.system().lower() == 'windows': - colorize_console = os.isatty(sys.stdout.fileno()) and _windows_ansi() # type: bool -else: - colorize_console = os.isatty(sys.stdout.fileno()) and os.environ.get('TERM') != 'dumb' +try: + if platform.system().lower() == 'windows': + colorize_console = os.isatty(sys.stdout.fileno()) and _windows_ansi() # type: bool + else: + colorize_console = os.isatty(sys.stdout.fileno()) and os.environ.get('TERM') != 'dumb' +except Exception: + colorize_console = False log_dir = None # type: Optional[str] log_file = None # type: Optional[TextIO] log_fname = 'meson-log.txt' # type: str diff --git a/mesonbuild/scripts/meson_exe.py b/mesonbuild/scripts/meson_exe.py index 85dfe99..3fe327f 100644 --- a/mesonbuild/scripts/meson_exe.py +++ b/mesonbuild/scripts/meson_exe.py @@ -78,6 +78,11 @@ def run_exe(exe): stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() + + if p.returncode == 0xc0000135: + # STATUS_DLL_NOT_FOUND on Windows indicating a common problem that is otherwise hard to diagnose + raise FileNotFoundError('Missing DLLs on calling {!r}'.format(exe.name)) + if exe.capture and p.returncode == 0: with open(exe.capture, 'wb') as output: output.write(stdout) diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..aacfb3f --- /dev/null +++ b/mypy.ini @@ -0,0 +1,4 @@ +[mypy] +strict_optional = False +show_error_context = False +show_column_numbers = True
\ No newline at end of file diff --git a/run_meson_command_tests.py b/run_meson_command_tests.py index e7eab72..9dfb62e 100755 --- a/run_meson_command_tests.py +++ b/run_meson_command_tests.py @@ -142,6 +142,8 @@ class CommandTests(unittest.TestCase): s = p.as_posix() if 'mesonbuild' not in s: continue + if '/data/' in s: + continue have.add(s[s.rfind('mesonbuild'):]) self.assertEqual(have, expect) # Run `meson` diff --git a/run_project_tests.py b/run_project_tests.py index 3bd3253..797e1a9 100755 --- a/run_project_tests.py +++ b/run_project_tests.py @@ -766,19 +766,14 @@ def _run_tests(all_tests, log_name_base, failfast, extra_args): ET.ElementTree(element=junit_root).write(xmlname, xml_declaration=True, encoding='UTF-8') return passing_tests, failing_tests, skipped_tests -def check_file(fname): - linenum = 1 - with open(fname, 'rb') as f: - lines = f.readlines() +def check_file(file: Path): + lines = file.read_bytes().split(b'\n') tabdetector = re.compile(br' *\t') - for line in lines: + for i, line in enumerate(lines): if re.match(tabdetector, line): - print("File %s contains a tab indent on line %d. Only spaces are permitted." % (fname, linenum)) - sys.exit(1) - if b'\r' in line: - print("File %s contains DOS line ending on line %d. Only unix-style line endings are permitted." % (fname, linenum)) - sys.exit(1) - linenum += 1 + raise SystemExit("File {} contains a tab indent on line {:d}. Only spaces are permitted.".format(file, i + 1)) + if line.endswith(b'\r'): + raise SystemExit("File {} contains DOS line ending on line {:d}. Only unix-style line endings are permitted.".format(file, i + 1)) def check_format(): check_suffixes = {'.c', @@ -800,18 +795,21 @@ def check_format(): '.build', '.md', } - for (root, _, files) in os.walk('.'): + for (root, _, filenames) in os.walk('.'): if '.dub' in root: # external deps are here continue + if '.pytest_cache' in root: + continue if 'meson-logs' in root or 'meson-private' in root: continue - for fname in files: - if os.path.splitext(fname)[1].lower() in check_suffixes: - bn = os.path.basename(fname) - if bn == 'sitemap.txt' or bn == 'meson-test-run.txt': + if '.eggs' in root or '_cache' in root: # e.g. .mypy_cache + continue + for fname in filenames: + file = Path(fname) + if file.suffix.lower() in check_suffixes: + if file.name in ('sitemap.txt', 'meson-test-run.txt'): continue - fullname = os.path.join(root, fname) - check_file(fullname) + check_file(root / file) def check_meson_commands_work(): global backend, compile_commands, test_commands, install_commands @@ -870,6 +868,8 @@ if __name__ == '__main__': choices=backendlist) parser.add_argument('--failfast', action='store_true', help='Stop running if test case fails') + parser.add_argument('--no-unittests', action='store_true', + help='Not used, only here to simplify run_tests.py') parser.add_argument('--only', help='name of test(s) to run', nargs='+') options = parser.parse_args() setup_commands(options.backend) @@ -901,4 +901,4 @@ if __name__ == '__main__': tests = list(g) if len(tests) != 1: print('WARNING: The %s suite contains duplicate "%s" tests: "%s"' % (name, k, '", "'.join(tests))) - sys.exit(failing_tests) + raise SystemExit(failing_tests) diff --git a/run_tests.py b/run_tests.py index f427736..051b91e 100755 --- a/run_tests.py +++ b/run_tests.py @@ -262,6 +262,7 @@ def main(): choices=backendlist) parser.add_argument('--cross', default=False, dest='cross', action='store_true') parser.add_argument('--failfast', action='store_true') + parser.add_argument('--no-unittests', action='store_true', default=False) (options, _) = parser.parse_known_args() # Enable coverage early... enable_coverage = options.cov @@ -273,6 +274,7 @@ def main(): returncode = 0 cross = options.cross backend, _ = guess_backend(options.backend, shutil.which('msbuild')) + no_unittests = options.no_unittests # Running on a developer machine? Be nice! if not mesonlib.is_windows() and not mesonlib.is_haiku() and 'CI' not in os.environ: os.nice(20) @@ -314,12 +316,16 @@ def main(): returncode += subprocess.call(cmd, env=env) if options.failfast and returncode != 0: return returncode - cmd = mesonlib.python_command + ['run_unittests.py', '-v'] - if options.failfast: - cmd += ['--failfast'] - returncode += subprocess.call(cmd, env=env) - if options.failfast and returncode != 0: - return returncode + if no_unittests: + print('Skipping all unit tests.') + returncode = 0 + else: + cmd = mesonlib.python_command + ['run_unittests.py', '-v'] + if options.failfast: + cmd += ['--failfast'] + returncode += subprocess.call(cmd, env=env) + if options.failfast and returncode != 0: + return returncode cmd = mesonlib.python_command + ['run_project_tests.py'] + sys.argv[1:] returncode += subprocess.call(cmd, env=env) else: diff --git a/run_unittests.py b/run_unittests.py index dbfe16d..d4e6735 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -3400,7 +3400,7 @@ recommended as it is not supported on some platforms''') for entry in res: name = entry['name'] - self.assertEquals(entry['subproject'], expected[name]) + self.assertEqual(entry['subproject'], expected[name]) def test_introspect_projectinfo_subproject_dir(self): testdir = os.path.join(self.common_test_dir, '79 custom subproject dir') @@ -3749,6 +3749,19 @@ recommended as it is not supported on some platforms''') testdir = os.path.join(self.unit_test_dir, '61 cmake parser') self.init(testdir, extra_args=['-Dcmake_prefix_path=' + os.path.join(testdir, 'prefix')]) + def test_alias_target(self): + if self.backend is Backend.vs: + # FIXME: This unit test is broken with vs backend, needs investigation + raise unittest.SkipTest('Skipping alias_target test with {} backend'.format(self.backend.name)) + testdir = os.path.join(self.unit_test_dir, '62 alias target') + self.init(testdir) + self.build() + self.assertPathDoesNotExist(os.path.join(self.builddir, 'prog' + exe_suffix)) + self.assertPathDoesNotExist(os.path.join(self.builddir, 'hello.txt')) + self.run_target('build-all') + self.assertPathExists(os.path.join(self.builddir, 'prog' + exe_suffix)) + self.assertPathExists(os.path.join(self.builddir, 'hello.txt')) + class FailureTests(BasePlatformTests): ''' Tests that test failure conditions. Build files here should be dynamically @@ -5901,7 +5914,7 @@ class NativeFileTests(BasePlatformTests): if mesonbuild.environment.detect_msys2_arch(): f.write(r'@python3 {} %*'.format(filename)) else: - f.write('@py -3 {} %*'.format(filename)) + f.write('@{} {} %*'.format(sys.executable, filename)) return batfile def helper_for_compiler(self, lang, cb, for_machine = MachineChoice.HOST): @@ -6524,6 +6537,17 @@ def unset_envs(): def main(): unset_envs() + pytest_args = ['-n', 'auto', './run_unittests.py'] + if shutil.which('pytest-3'): + return subprocess.run(['pytest-3'] + pytest_args).returncode + elif shutil.which('pytest'): + return subprocess.run(['pytest'] + pytest_args).returncode + try: + import pytest # noqa: F401 + return subprocess.run(python_command + ['-m', 'pytest'] + pytest_args).returncode + except ImportError: + pass + # All attempts at locating pytest failed, fall back to plain unittest. cases = ['InternalTests', 'DataTests', 'AllPlatformTests', 'FailureTests', 'PythonTests', 'NativeFileTests', 'RewriterTests', 'CrossFileTests', 'TAPParserTests', @@ -1,33 +1,32 @@ -[flake8] -ignore = - # E241: multiple spaces after ':' - E241, - # E251: unexpected spaces around keyword / parameter equals - E251, - # E261: at least two spaces before inline comment - E261, - # E265: block comment should start with '# ' - E265, - # E501: line too long - E501, - # E302: expected 2 blank lines, found 1 - E302, - # E305: expected 2 blank lines after class or function definition, found 1 - E305, - # E401: multiple imports on one line - E401, - # E266: too many leading '#' for block comment - E266, - # E402: module level import not at top of file - E402, - # E731: do not assign a lambda expression, use a def (too many false positives) - E731 - # E741: ambiguous variable name 'l' - E741 - # E722: do not use bare except' - E722 - # W504: line break after binary operator - W504 - # A003: builtin class attribute - A003 -max-line-length = 120 +[metadata] +description = A high performance build system +author = Jussi Pakkanen +author_email = jpakkane@gmail.com +url = https://mesonbuild.com +keywords = + meson + mesonbuild + build system + cmake +license = Apache License, Version 2.0 +license_file = COPYING +classifiers = + Development Status :: 5 - Production/Stable + Environment :: Console + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Natural Language :: English + Operating System :: MacOS :: MacOS X + Operating System :: Microsoft :: Windows + Operating System :: POSIX :: BSD + Operating System :: POSIX :: Linux + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Topic :: Software Development :: Build Tools +long_description = Meson is a cross-platform build system designed to be both as fast and as user friendly as possible. It supports many languages and compilers, including GCC, Clang, PGI, Intel, and Visual Studio. Its build definitions are written in a simple non-Turing complete DSL. + +[options] +python_requires = >= 3.5.2 @@ -16,10 +16,9 @@ import sys -if sys.version_info < (3, 5, 0): - print('Tried to install with an unsupported version of Python. ' - 'Meson requires Python 3.5.0 or greater') - sys.exit(1) +if sys.version_info < (3, 5, 2): + raise SystemExit('ERROR: Tried to install Meson with an unsupported Python version: \n{}' + '\nMeson requires Python 3.5.2 or greater'.format(sys.version)) from mesonbuild.coredata import version from setuptools import setup @@ -36,7 +35,10 @@ packages = ['mesonbuild', 'mesonbuild.modules', 'mesonbuild.scripts', 'mesonbuild.wrap'] -package_data = {'mesonbuild.dependencies': ['data/CMakeLists.txt', 'data/CMakeListsLLVM.txt', 'data/CMakePathInfo.txt']} +package_data = { + 'mesonbuild.dependencies': ['data/CMakeLists.txt', 'data/CMakeListsLLVM.txt', 'data/CMakePathInfo.txt'], + 'mesonbuild.cmake': ['data/run_ctgt.py'], +} data_files = [] if sys.platform != 'win32': # Only useful on UNIX-like systems @@ -46,29 +48,7 @@ if sys.platform != 'win32': if __name__ == '__main__': setup(name='meson', version=version, - description='A high performance build system', - author='Jussi Pakkanen', - author_email='jpakkane@gmail.com', - url='http://mesonbuild.com', - license=' Apache License, Version 2.0', - python_requires='>=3.5', packages=packages, package_data=package_data, entry_points=entries, - data_files=data_files, - classifiers=['Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Natural Language :: English', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX :: BSD', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python :: 3 :: Only', - 'Topic :: Software Development :: Build Tools', - ], - long_description='''Meson is a cross-platform build system designed to be both as - fast and as user friendly as possible. It supports many languages and compilers, including - GCC, Clang and Visual Studio. Its build definitions are written in a simple non-turing - complete DSL.''') + data_files=data_files,) diff --git a/test cases/cmake/8 custom command/main.cpp b/test cases/cmake/8 custom command/main.cpp new file mode 100644 index 0000000..fa6b0fa --- /dev/null +++ b/test cases/cmake/8 custom command/main.cpp @@ -0,0 +1,11 @@ +#include <iostream> +#include <cmMod.hpp> + +using namespace std; + +int main() { + cmModClass obj("Hello"); + cout << obj.getStr() << endl; + cout << obj.getOther() << endl; + return 0; +} diff --git a/test cases/cmake/8 custom command/meson.build b/test cases/cmake/8 custom command/meson.build new file mode 100644 index 0000000..799e339 --- /dev/null +++ b/test cases/cmake/8 custom command/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_type('cmModLib') == 'shared_library', 'Target type should be shared_library') +assert(sub_pro.target_type('gen') == 'executable', 'Target type should be executable') + +exe1 = executable('main', ['main.cpp'], dependencies: [sub_dep]) +test('test1', exe1) diff --git a/test cases/cmake/8 custom command/subprojects/cmMod/CMakeLists.txt b/test cases/cmake/8 custom command/subprojects/cmMod/CMakeLists.txt new file mode 100644 index 0000000..259151c --- /dev/null +++ b/test cases/cmake/8 custom command/subprojects/cmMod/CMakeLists.txt @@ -0,0 +1,46 @@ +cmake_minimum_required(VERSION 3.5) + +project(cmMod) +set (CMAKE_CXX_STANDARD 14) + +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +add_definitions("-DDO_NOTHING_JUST_A_FLAG=1") + +add_executable(gen main.cpp) +add_executable(mycpy cp.cpp) + +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/genTest.cpp" "${CMAKE_CURRENT_BINARY_DIR}/genTest.hpp" + COMMAND gen ARGS genTest +) + +add_custom_command( + OUTPUT cpyBase.cpp + COMMAND mycpy "${CMAKE_CURRENT_SOURCE_DIR}/cpyBase.cpp.am" cpyBase.cpp.in + COMMAND mycpy cpyBase.cpp.in cpyBase.cpp.something + COMMAND mycpy cpyBase.cpp.something cpyBase.cpp.IAmRunningOutOfIdeas + COMMAND mycpy cpyBase.cpp.IAmRunningOutOfIdeas cpyBase.cpp + DEPENDS cpyBase.cpp.am gen +) + +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/cpyBase.hpp.in" + COMMAND mycpy "${CMAKE_CURRENT_SOURCE_DIR}/cpyBase.hpp.am" cpyBase.hpp.in + DEPENDS cpyBase.hpp.am +) + +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/cpyBase.hpp.something" + COMMAND mycpy cpyBase.hpp.in cpyBase.hpp.something + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/cpyBase.hpp.in" +) + +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/cpyBase.hpp" + COMMAND mycpy cpyBase.hpp.something cpyBase.hpp + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/cpyBase.hpp.something" +) + +add_library(cmModLib SHARED cmMod.cpp genTest.cpp cpyBase.cpp cpyBase.hpp) +include(GenerateExportHeader) +generate_export_header(cmModLib) diff --git a/test cases/cmake/8 custom command/subprojects/cmMod/cmMod.cpp b/test cases/cmake/8 custom command/subprojects/cmMod/cmMod.cpp new file mode 100644 index 0000000..0fb6aa7 --- /dev/null +++ b/test cases/cmake/8 custom command/subprojects/cmMod/cmMod.cpp @@ -0,0 +1,17 @@ +#include "cmMod.hpp" +#include "genTest.hpp" +#include "cpyBase.hpp" + +using namespace std; + +cmModClass::cmModClass(string foo) { + str = foo + " World"; +} + +string cmModClass::getStr() const { + return str; +} + +string cmModClass::getOther() const { + return getStr() + " -- " + getStrCpy(); +} diff --git a/test cases/cmake/8 custom command/subprojects/cmMod/cmMod.hpp b/test cases/cmake/8 custom command/subprojects/cmMod/cmMod.hpp new file mode 100644 index 0000000..cfdbe88 --- /dev/null +++ b/test cases/cmake/8 custom command/subprojects/cmMod/cmMod.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include <string> +#include "cmmodlib_export.h" + +class CMMODLIB_EXPORT cmModClass { + private: + std::string str; + public: + cmModClass(std::string foo); + + std::string getStr() const; + std::string getOther() const; +}; diff --git a/test cases/cmake/8 custom command/subprojects/cmMod/cp.cpp b/test cases/cmake/8 custom command/subprojects/cmMod/cp.cpp new file mode 100644 index 0000000..2744da8 --- /dev/null +++ b/test cases/cmake/8 custom command/subprojects/cmMod/cp.cpp @@ -0,0 +1,17 @@ +#include <iostream> +#include <fstream> + +using namespace std; + +int main(int argc, char *argv[]) { + if(argc < 3) { + cerr << argv[0] << " requires an input and an output file!" << endl; + return 1; + } + + ifstream src(argv[1]); + ofstream dst(argv[2]); + + dst << src.rdbuf(); + return 0; +} diff --git a/test cases/cmake/8 custom command/subprojects/cmMod/cpyBase.cpp.am b/test cases/cmake/8 custom command/subprojects/cmMod/cpyBase.cpp.am new file mode 100644 index 0000000..98dd09c --- /dev/null +++ b/test cases/cmake/8 custom command/subprojects/cmMod/cpyBase.cpp.am @@ -0,0 +1,5 @@ +#include "cpyBase.hpp" + +std::string getStrCpy() { + return "Hello Copied File"; +} diff --git a/test cases/cmake/8 custom command/subprojects/cmMod/cpyBase.hpp.am b/test cases/cmake/8 custom command/subprojects/cmMod/cpyBase.hpp.am new file mode 100644 index 0000000..c255fb1 --- /dev/null +++ b/test cases/cmake/8 custom command/subprojects/cmMod/cpyBase.hpp.am @@ -0,0 +1,5 @@ +#pragma once + +#include <string> + +std::string getStrCpy(); diff --git a/test cases/cmake/8 custom command/subprojects/cmMod/main.cpp b/test cases/cmake/8 custom command/subprojects/cmMod/main.cpp new file mode 100644 index 0000000..9fade21 --- /dev/null +++ b/test cases/cmake/8 custom command/subprojects/cmMod/main.cpp @@ -0,0 +1,30 @@ +#include <iostream> +#include <fstream> + +using namespace std; + +int main(int argc, const char *argv[]) { + if(argc < 2) { + cerr << argv[0] << " requires an output file!" << endl; + return 1; + } + ofstream out1(string(argv[1]) + ".hpp"); + ofstream out2(string(argv[1]) + ".cpp"); + out1 << R"( +#pragma once + +#include <string> + +std::string getStr(); +)"; + + out2 << R"( +#include ")" << argv[1] << R"(.hpp" + +std::string getStr() { + return "Hello World"; +} +)"; + + return 0; +} diff --git a/test cases/frameworks/1 boost/meson.build b/test cases/frameworks/1 boost/meson.build index 1d29455..8f45dc7 100644 --- a/test cases/frameworks/1 boost/meson.build +++ b/test cases/frameworks/1 boost/meson.build @@ -26,18 +26,71 @@ testdep = dependency('boost', modules : ['unit_test_framework']) nomoddep = dependency('boost') extralibdep = dependency('boost', modules : ['thread', 'system', 'log_setup', 'log']) +pymod = import('python') +python2 = pymod.find_installation('python2', required: host_machine.system() == 'linux', disabler: true) +python3 = pymod.find_installation('python3', required: host_machine.system() == 'linux', disabler: true) +python2dep = python2.dependency(required: host_machine.system() == 'linux', disabler: true) +python3dep = python3.dependency(required: host_machine.system() == 'linux', disabler: true) + +# compile python 2/3 modules only if we found a corresponding python version +if(python2dep.found() and host_machine.system() == 'linux') + if(dep.version().version_compare('>=1.67')) + # if we have a new version of boost, we need to construct the module name based + # on the installed version of python (and hope that they match the version boost + # was compiled against) + py2version_string = ''.join(python2dep.version().split('.')) + bpython2dep = dependency('boost', modules : ['python' + py2version_string]) + else + # if we have an older version of boost, we need to use the old module names + bpython2dep = dependency('boost', modules : ['python']) + endif + + if not (bpython2dep.found()) + bpython2dep = disabler() + endif +else + python2dep = disabler() + bpython2dep = disabler() +endif + +if(python3dep.found() and host_machine.system() == 'linux') + if(dep.version().version_compare('>=1.67')) + py3version_string = ''.join(python3dep.version().split('.')) + bpython3dep = dependency('boost', modules : ['python' + py3version_string]) + else + bpython3dep = dependency('boost', modules : ['python3']) + endif + + if not (bpython3dep.found()) + bpython3dep = disabler() + endif +else + python3dep = disabler() + bpython3dep = disabler() +endif + linkexe = executable('linkedexe', 'linkexe.cc', dependencies : linkdep) staticexe = executable('staticlinkedexe', 'linkexe.cc', dependencies : staticdep) unitexe = executable('utf', 'unit_test.cpp', dependencies: testdep) nomodexe = executable('nomod', 'nomod.cpp', dependencies : nomoddep) extralibexe = executable('extralibexe', 'extralib.cpp', dependencies : extralibdep) +# python modules are shared libraries +python2module = shared_library('python2_module', ['python_module.cpp'], dependencies: [python2dep, bpython2dep], name_prefix: '', cpp_args: ['-DMOD_NAME=python2_module']) +python3module = shared_library('python3_module', ['python_module.cpp'], dependencies: [python3dep, bpython3dep], name_prefix: '', cpp_args: ['-DMOD_NAME=python3_module']) + test('Boost linktest', linkexe) test('Boost statictest', staticexe) test('Boost UTF test', unitexe) test('Boost nomod', nomodexe) test('Boost extralib test', extralibexe) +# explicitly use the correct python interpreter so that we don't have to provide two different python scripts that have different shebang lines +python2interpreter = find_program(python2.path(), required: false, disabler: true) +test('Boost Python2', python2interpreter, args: ['./test_python_module.py', meson.current_build_dir()], workdir: meson.current_source_dir(), depends: python2module) +python3interpreter = find_program(python3.path(), required: false, disabler: true) +test('Boost Python3', python3interpreter, args: ['./test_python_module.py', meson.current_build_dir()], workdir: meson.current_source_dir(), depends: python2module) + subdir('partial_dep') # check we can apply a version constraint diff --git a/test cases/frameworks/1 boost/python_module.cpp b/test cases/frameworks/1 boost/python_module.cpp new file mode 100644 index 0000000..a0f010b --- /dev/null +++ b/test cases/frameworks/1 boost/python_module.cpp @@ -0,0 +1,22 @@ +#define PY_SSIZE_T_CLEAN +#include <Python.h> +#include <boost/python.hpp> + +struct World +{ + void set(std::string msg) { this->msg = msg; } + std::string greet() { return msg; } + std::string version() { return std::to_string(PY_MAJOR_VERSION) + "." + std::to_string(PY_MINOR_VERSION); } + std::string msg; +}; + + +BOOST_PYTHON_MODULE(MOD_NAME) +{ + using namespace boost::python; + class_<World>("World") + .def("greet", &World::greet) + .def("set", &World::set) + .def("version", &World::version) + ; +} diff --git a/test cases/frameworks/1 boost/test_python_module.py b/test cases/frameworks/1 boost/test_python_module.py new file mode 100644 index 0000000..acf6e42 --- /dev/null +++ b/test cases/frameworks/1 boost/test_python_module.py @@ -0,0 +1,27 @@ +import sys +sys.path.append(sys.argv[1]) + +# import compiled python module depending on version of python we are running with +if sys.version_info[0] == 2: + import python2_module + +if sys.version_info[0] == 3: + import python3_module + + +def run(): + msg = 'howdy' + if sys.version_info[0] == 2: + w = python2_module.World() + + if sys.version_info[0] == 3: + w = python3_module.World() + + w.set(msg) + + assert(msg == w.greet()) + version_string = str(sys.version_info[0]) + "." + str(sys.version_info[1]) + assert(version_string == w.version()) + +if __name__ == '__main__': + run() diff --git a/test cases/linuxlike/13 cmake dependency/cmake/FindSomethingLikeZLIB.cmake b/test cases/linuxlike/13 cmake dependency/cmake/FindSomethingLikeZLIB.cmake index a2f8456..9e68ac6 100644 --- a/test cases/linuxlike/13 cmake dependency/cmake/FindSomethingLikeZLIB.cmake +++ b/test cases/linuxlike/13 cmake dependency/cmake/FindSomethingLikeZLIB.cmake @@ -1,5 +1,8 @@ find_package(ZLIB) +include(CMakeFindDependencyMacro) +find_dependency(Threads) + if(ZLIB_FOUND OR ZLIB_Found) set(SomethingLikeZLIB_FOUND ON) set(SomethingLikeZLIB_LIBRARIES ${ZLIB_LIBRARY}) diff --git a/test cases/unit/62 alias target/main.c b/test cases/unit/62 alias target/main.c new file mode 100644 index 0000000..0fb4389 --- /dev/null +++ b/test cases/unit/62 alias target/main.c @@ -0,0 +1,3 @@ +int main(int argc, char *argv[]) { + return 0; +} diff --git a/test cases/unit/62 alias target/meson.build b/test cases/unit/62 alias target/meson.build new file mode 100644 index 0000000..6934cc7 --- /dev/null +++ b/test cases/unit/62 alias target/meson.build @@ -0,0 +1,15 @@ +project('alias target', 'c') + +python3 = import('python').find_installation() + +exe_target = executable('prog', 'main.c', + build_by_default : false) + +custom_target = custom_target('custom-target', + output : 'hello.txt', + command : [python3, '-c', 'print("hello")'], + capture : true, + build_by_default : false +) + +alias_target('build-all', [exe_target, custom_target]) |