diff options
author | Jussi Pakkanen <jpakkane@gmail.com> | 2019-07-10 19:31:17 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-07-10 19:31:17 +0300 |
commit | 4751f6d854a948c49dec3157d9e38b198f0e3ee8 (patch) | |
tree | ed8847044f82f5fbecaeb24b7482f9857f7bed86 | |
parent | 724113849c1eed224a0a1edea70a9cc4bab93229 (diff) | |
parent | 8320217210925344faf01928a8b04f5b39cda1e4 (diff) | |
download | meson-4751f6d854a948c49dec3157d9e38b198f0e3ee8.zip meson-4751f6d854a948c49dec3157d9e38b198f0e3ee8.tar.gz meson-4751f6d854a948c49dec3157d9e38b198f0e3ee8.tar.bz2 |
Merge pull request #5574 from mensinda/cmakeCCmd
CMake subprojects add_custom_command support
-rw-r--r-- | mesonbuild/ast/printer.py | 3 | ||||
-rw-r--r-- | mesonbuild/ast/visitor.py | 1 | ||||
-rwxr-xr-x | mesonbuild/cmake/data/run_ctgt.py | 59 | ||||
-rw-r--r-- | mesonbuild/cmake/interpreter.py | 335 | ||||
-rw-r--r-- | mesonbuild/cmake/traceparser.py | 140 | ||||
-rwxr-xr-x | run_meson_command_tests.py | 2 | ||||
-rw-r--r-- | setup.py | 5 | ||||
-rw-r--r-- | test cases/cmake/8 custom command/main.cpp | 11 | ||||
-rw-r--r-- | test cases/cmake/8 custom command/meson.build | 12 | ||||
-rw-r--r-- | test cases/cmake/8 custom command/subprojects/cmMod/CMakeLists.txt | 46 | ||||
-rw-r--r-- | test cases/cmake/8 custom command/subprojects/cmMod/cmMod.cpp | 17 | ||||
-rw-r--r-- | test cases/cmake/8 custom command/subprojects/cmMod/cmMod.hpp | 14 | ||||
-rw-r--r-- | test cases/cmake/8 custom command/subprojects/cmMod/cp.cpp | 17 | ||||
-rw-r--r-- | test cases/cmake/8 custom command/subprojects/cmMod/cpyBase.cpp.am | 5 | ||||
-rw-r--r-- | test cases/cmake/8 custom command/subprojects/cmMod/cpyBase.hpp.am | 5 | ||||
-rw-r--r-- | test cases/cmake/8 custom command/subprojects/cmMod/main.cpp | 30 |
16 files changed, 656 insertions, 46 deletions
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/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/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/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` @@ -36,7 +36,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 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; +} |