aboutsummaryrefslogtreecommitdiff
path: root/mesonbuild
diff options
context:
space:
mode:
authorJussi Pakkanen <jpakkane@gmail.com>2019-07-10 19:31:17 +0300
committerGitHub <noreply@github.com>2019-07-10 19:31:17 +0300
commit4751f6d854a948c49dec3157d9e38b198f0e3ee8 (patch)
treeed8847044f82f5fbecaeb24b7482f9857f7bed86 /mesonbuild
parent724113849c1eed224a0a1edea70a9cc4bab93229 (diff)
parent8320217210925344faf01928a8b04f5b39cda1e4 (diff)
downloadmeson-4751f6d854a948c49dec3157d9e38b198f0e3ee8.zip
meson-4751f6d854a948c49dec3157d9e38b198f0e3ee8.tar.gz
meson-4751f6d854a948c49dec3157d9e38b198f0e3ee8.tar.bz2
Merge pull request #5574 from mensinda/cmakeCCmd
CMake subprojects add_custom_command support
Diffstat (limited to 'mesonbuild')
-rw-r--r--mesonbuild/ast/printer.py3
-rw-r--r--mesonbuild/ast/visitor.py1
-rwxr-xr-xmesonbuild/cmake/data/run_ctgt.py59
-rw-r--r--mesonbuild/cmake/interpreter.py335
-rw-r--r--mesonbuild/cmake/traceparser.py140
5 files changed, 493 insertions, 45 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