Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions mesonbuild/backend/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,11 @@ def eval_custom_target_command(self, target, absolute_outputs=False):
i = i.replace('@SOURCE_ROOT@', source_root)
elif '@BUILD_ROOT@' in i:
i = i.replace('@BUILD_ROOT@', build_root)
elif '@OUTDIR@' in i:
outdir = target.subdir
if outdir == '':
outdir = '.'
i = i.replace('@OUTDIR@', outdir)
elif '@DEPFILE@' in i:
if target.depfile is None:
msg = 'Custom target {!r} has @DEPFILE@ but no depfile ' \
Expand Down
24 changes: 15 additions & 9 deletions mesonbuild/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .mesonlib import File, MesonException, listify, extract_as_list
from .mesonlib import typeslistify, stringlistify, classify_unity_sources
from .mesonlib import get_filenames_templates_dict, substitute_values
from .mesonlib import path_norm_split
from .environment import for_windows, for_darwin, for_cygwin
from .compilers import is_object, clike_langs, sort_clike, lang_suffixes

Expand Down Expand Up @@ -1051,10 +1052,10 @@ def process_kwargs(self, kwargs):
for rule in outputs:
if not isinstance(rule, str):
raise InvalidArguments('"output" may only contain strings.')
if '@BASENAME@' not in rule and '@PLAINNAME@' not in rule:
raise InvalidArguments('Every element of "output" must contain @BASENAME@ or @PLAINNAME@.')
if '/' in rule or '\\' in rule:
raise InvalidArguments('"outputs" must not contain a directory separator.')
if '@BASENAME@' not in rule and '@PLAINNAME@' not in rule and '@ROOTNAME@' not in rule:
raise InvalidArguments('Every element of "output" must contain @BASENAME@, @PLAINNAME@ or @ROOTNAME@.')
if '..' in path_norm_split(rule):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should just use pathlib.PurePath and then check .parts for ...

raise InvalidArguments('Output file path name must not contain a .. component.')
if len(outputs) > 1:
for o in outputs:
if '@OUTPUT@' in o:
Expand All @@ -1076,19 +1077,22 @@ def process_kwargs(self, kwargs):
def get_base_outnames(self, inname):
plainname = os.path.split(inname)[1]
basename = os.path.splitext(plainname)[0]
return [x.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname) for x in self.outputs]
rootname = os.path.splitext(inname)[0]
return [x.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname).replace('@ROOTNAME@', rootname) for x in self.outputs]

def get_dep_outname(self, inname):
if self.depfile is None:
raise InvalidArguments('Tried to get dep name for rule that does not have dependency file defined.')
plainname = os.path.split(inname)[1]
basename = os.path.splitext(plainname)[0]
return self.depfile.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname)
rootname = os.path.splitext(inname)[0]
return self.depfile.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname).replace('@ROOTNAME@', rootname)

def get_arglist(self, inname):
plainname = os.path.split(inname)[1]
basename = os.path.splitext(plainname)[0]
return [x.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname) for x in self.arglist]
rootname = os.path.splitext(inname)[0]
return [x.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname).replace('@ROOTNAME@', rootname) for x in self.arglist]

def process_files(self, name, files, state, extra_args=[]):
output = GeneratedList(self, extra_args=extra_args)
Expand Down Expand Up @@ -1585,8 +1589,10 @@ def process_kwargs(self, kwargs):
for i in self.outputs:
if not(isinstance(i, str)):
raise InvalidArguments('Output argument not a string.')
if '/' in i:
raise InvalidArguments('Output must not contain a path segment.')
if os.path.isabs(i):
raise InvalidArguments('Output file path name must not be an absolute path.')
if '..' in path_norm_split(i):
raise InvalidArguments('Output file path name must not contain a .. component.')
if '@INPUT@' in i or '@INPUT0@' in i:
m = 'Output cannot contain @INPUT@ or @INPUT0@, did you ' \
'mean @PLAINNAME@ or @BASENAME@?'
Expand Down
8 changes: 5 additions & 3 deletions mesonbuild/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2617,17 +2617,19 @@ def func_configure_file(self, node, args, kwargs):
values = mesonlib.get_filenames_templates_dict([ifile_abs], None)
outputs = mesonlib.substitute_values([output], values)
output = outputs[0]
if os.path.split(output)[0] != '':
raise InterpreterException('Output file name must not contain a subdirectory.')
if os.path.isabs(output):
raise InterpreterException('Output file path name must not be an absolute path.')
if '..' in mesonlib.path_norm_split(output):
raise InterpreterException('Output file path name must not contain a .. component.')
(ofile_path, ofile_fname) = os.path.split(os.path.join(self.subdir, output))
ofile_abs = os.path.join(self.environment.build_dir, ofile_path, ofile_fname)
os.makedirs(os.path.join(self.environment.build_dir, ofile_path), exist_ok=True)
if 'configuration' in kwargs:
conf = kwargs['configuration']
if not isinstance(conf, ConfigurationDataHolder):
raise InterpreterException('Argument "configuration" is not of type configuration_data')
mlog.log('Configuring', mlog.bold(output), 'using configuration')
if inputfile is not None:
os.makedirs(os.path.join(self.environment.build_dir, self.subdir), exist_ok=True)
missing_variables = mesonlib.do_conf_file(ifile_abs, ofile_abs,
conf.held_object)
if missing_variables:
Expand Down
27 changes: 18 additions & 9 deletions mesonbuild/mesonlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,19 @@ def expand_arguments(args):
return None
return expended_args

def path_norm_split(path):
'''
Normalize a path and split into a list of components. First component of an absolute
path will be os.sep
Drive will be discarded.
'''
_, path = os.path.splitdrive(path)
norm_path = os.path.normpath(path)
components = norm_path.split(os.sep)
if components[0] == '':
components[0] = os.sep
return components

def Popen_safe(args, write=None, stderr=subprocess.PIPE, **kwargs):
if sys.version_info < (3, 6) or not sys.stdout.encoding:
return Popen_safe_legacy(args, write=write, stderr=stderr, **kwargs)
Expand Down Expand Up @@ -610,8 +623,8 @@ def iter_regexin_iter(regexiter, initer):

def _substitute_values_check_errors(command, values):
# Error checking
inregex = ('@INPUT([0-9]+)?@', '@PLAINNAME@', '@BASENAME@')
outregex = ('@OUTPUT([0-9]+)?@', '@OUTDIR@')
inregex = ('@INPUT([0-9]+)?@', '@PLAINNAME@', '@BASENAME@', '@ROOTNAME@')
outregex = ('@OUTPUT([0-9]+)?@',)
if '@INPUT@' not in values:
# Error out if any input-derived templates are present in the command
match = iter_regexin_iter(inregex, command)
Expand All @@ -620,7 +633,7 @@ def _substitute_values_check_errors(command, values):
raise MesonException(m.format(match))
else:
if len(values['@INPUT@']) > 1:
# Error out if @PLAINNAME@ or @BASENAME@ is present in the command
# Error out if @PLAINNAME@, @BASENAME@ or @ROOTNAME@ is present in the command
match = iter_regexin_iter(inregex[1:], command)
if match:
raise MesonException('Command cannot have {!r} when there is '
Expand Down Expand Up @@ -705,12 +718,12 @@ def get_filenames_templates_dict(inputs, outputs):

@INPUT@ - the full path to one or more input files, from @inputs
@OUTPUT@ - the full path to one or more output files, from @outputs
@OUTDIR@ - the full path to the directory containing the output files

If there is only one input file, the following keys are also created:

@PLAINNAME@ - the filename of the input file
@BASENAME@ - the filename of the input file with the extension removed
@ROOTNAME@ - the full path name of the input file with the extension removed

If there is more than one input file, the following keys are also created:

Expand All @@ -732,16 +745,12 @@ def get_filenames_templates_dict(inputs, outputs):
# Just one value, substitute @PLAINNAME@ and @BASENAME@
values['@PLAINNAME@'] = plain = os.path.split(inputs[0])[1]
values['@BASENAME@'] = os.path.splitext(plain)[0]
values['@ROOTNAME@'] = os.path.splitext(inputs[0])[0]
if outputs:
# Gather values derived from the outputs, similar to above.
values['@OUTPUT@'] = outputs
for (ii, vv) in enumerate(outputs):
values['@OUTPUT{}@'.format(ii)] = vv
# Outdir should be the same for all outputs
values['@OUTDIR@'] = os.path.split(outputs[0])[0]
# Many external programs fail on empty arguments.
if values['@OUTDIR@'] == '':
values['@OUTDIR@'] = '.'
return values


Expand Down
29 changes: 17 additions & 12 deletions run_unittests.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def test_string_templates_substitution(self):
outputs = []
ret = dictfunc(inputs, outputs)
d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
'@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c'}
'@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c', '@ROOTNAME@': "bar/foo.c"}
# Check dictionary
self.assertEqual(ret, d)
# Check substitutions
Expand All @@ -235,6 +235,9 @@ def test_string_templates_substitution(self):
cmd = ['@INPUT@', '@[email protected]', 'strings']
self.assertEqual(substfunc(cmd, d),
inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:])
cmd = ['@INPUT@', '@[email protected]', 'strings']
self.assertEqual(substfunc(cmd, d),
inputs + [d['@ROOTNAME@'] + '.bar'] + cmd[2:])
cmd = ['@OUTPUT@']
self.assertRaises(ME, substfunc, cmd, d)

Expand All @@ -243,8 +246,8 @@ def test_string_templates_substitution(self):
outputs = ['out.c']
ret = dictfunc(inputs, outputs)
d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
'@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c',
'@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': '.'}
'@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c', '@ROOTNAME@': 'bar/foo.c',
'@OUTPUT@': outputs, '@OUTPUT0@': outputs[0]}
# Check dictionary
self.assertEqual(ret, d)
# Check substitutions
Expand All @@ -259,13 +262,16 @@ def test_string_templates_substitution(self):
cmd = ['@INPUT@', '@[email protected]', 'strings']
self.assertEqual(substfunc(cmd, d),
inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:])
cmd = ['@INPUT@', '@[email protected]', 'strings']
self.assertEqual(substfunc(cmd, d),
inputs + [d['@ROOTNAME@'] + '.bar'] + cmd[2:])

# One input, one output with a subdir
outputs = ['dir/out.c']
ret = dictfunc(inputs, outputs)
d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
'@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c',
'@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': 'dir'}
'@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c', '@ROOTNAME@': 'bar/foo.c',
'@OUTPUT@': outputs, '@OUTPUT0@': outputs[0]}
# Check dictionary
self.assertEqual(ret, d)

Expand Down Expand Up @@ -298,19 +304,19 @@ def test_string_templates_substitution(self):
self.assertRaises(ME, substfunc, cmd, d)
cmd = ['@BASENAME@']
self.assertRaises(ME, substfunc, cmd, d)
cmd = ['@ROOTNAME@']
self.assertRaises(ME, substfunc, cmd, d)
# No outputs
cmd = ['@OUTPUT@']
self.assertRaises(ME, substfunc, cmd, d)
cmd = ['@OUTPUT0@']
self.assertRaises(ME, substfunc, cmd, d)
cmd = ['@OUTDIR@']
self.assertRaises(ME, substfunc, cmd, d)

# Two inputs, one output
outputs = ['dir/out.c']
ret = dictfunc(inputs, outputs)
d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1],
'@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': 'dir'}
'@OUTPUT@': outputs, '@OUTPUT0@': outputs[0]}
# Check dictionary
self.assertEqual(ret, d)
# Check substitutions
Expand All @@ -336,8 +342,7 @@ def test_string_templates_substitution(self):
outputs = ['dir/out.c', 'dir/out2.c']
ret = dictfunc(inputs, outputs)
d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1],
'@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTPUT1@': outputs[1],
'@OUTDIR@': 'dir'}
'@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTPUT1@': outputs[1]}
# Check dictionary
self.assertEqual(ret, d)
# Check substitutions
Expand All @@ -347,8 +352,8 @@ def test_string_templates_substitution(self):
self.assertEqual(substfunc(cmd, d), outputs + cmd[1:])
cmd = ['@OUTPUT0@', '@OUTPUT1@', 'strings']
self.assertEqual(substfunc(cmd, d), outputs + cmd[2:])
cmd = ['@[email protected]', '@[email protected]', '@OUTDIR@']
self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok', 'dir'])
cmd = ['@[email protected]', '@[email protected]']
self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok'])
# Many inputs, can't use @INPUT@ like this
cmd = ['@[email protected]', 'ordinary', 'strings']
self.assertRaises(ME, substfunc, cmd, d)
Expand Down
1 change: 1 addition & 0 deletions test cases/common/16 configure file/include/prjdir/foo.h
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// An ordinary header.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#define VERSION "@version@"
10 changes: 10 additions & 0 deletions test cases/common/16 configure file/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,13 @@ configs = [
foreach c : configs
test('@0@'.format(c), file_contains_py, args: [ c, test_string ])
endforeach

# Generally you don't want to install version.h directly under /usr/local/include,
# so we also test generating headers to a subdir of builddir
conf_version = configuration_data()
conf_version.set('version', '1.0.0')
configure_file(input : 'include/prjdir/version.h.in',
output : 'prjdir/version.h',
configuration: conf_version)
e5 = executable('prog5', 'prog5.c', include_directories: include_directories('include'))
test('inctest5', e5)
6 changes: 6 additions & 0 deletions test cases/common/16 configure file/prog5.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#include <prjdir/foo.h>
#include <prjdir/version.h>

int main(int argc, char **argv) {
return strcmp(VERSION, "1.0.0");
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
project('outdir path', 'c')

configure_file(input : 'foo.in',
output : 'subdir/foo',
output : '../subdir/foo',
configuration : configuration_data())
3 changes: 0 additions & 3 deletions test cases/frameworks/5 protocol buffers/asubdir/defs.proto

This file was deleted.

9 changes: 0 additions & 9 deletions test cases/frameworks/5 protocol buffers/asubdir/main.cpp

This file was deleted.

This file was deleted.

3 changes: 0 additions & 3 deletions test cases/frameworks/5 protocol buffers/defs.proto

This file was deleted.

9 changes: 0 additions & 9 deletions test cases/frameworks/5 protocol buffers/main.cpp

This file was deleted.

54 changes: 43 additions & 11 deletions test cases/frameworks/5 protocol buffers/meson.build
Original file line number Diff line number Diff line change
@@ -1,20 +1,52 @@
project('protocol buffer test', 'cpp')

protoc = find_program('protoc', required : false)
dep = dependency('protobuf', required : false)
protobuf_dep = dependency('protobuf')
protoc = find_program('protoc')

if not protoc.found() or not dep.found()
if not protoc.found() or not protobuf_dep.found()
error('MESON_SKIP_TEST: protoc tool and/or protobuf pkg-config dependency not found')
endif

# Builds a proto subtree from top_level meson.build.
# Currently build proto subtree from outside requires a little hack in order to get proto headers
# currectly on include paths. Building with subdir at the top level of proto subtree is a cleaner
# solution.
protos_basename = [
'messages/dummy_messages',
'dummy_service',
]
protos_src = []
protos_hdr = []
foreach p : protos_basename
cc_target = custom_target(
p.underscorify(),
input: 'proto/' + p + '.proto',
output: [p + '.pb.cc', p + '.pb.h'],
command: [protoc, '-I', '@CURRENT_SOURCE_DIR@/proto', '--cpp_out', '@OUTDIR@', '@INPUT@']
)
protos_src += cc_target[0]
protos_hdr += cc_target[1]
endforeach

gen = generator(protoc, \
output : ['@[email protected]', '@[email protected]'],
arguments : ['--proto_path=@SOURCE_DIR@', '--cpp_out=@BUILD_DIR@', '@INPUT@'])
# Builds another proto subtree in a subdir
subdir('proto_subdir')

generated = gen.process('defs.proto')
e = executable('prog', 'main.cpp', generated,
dependencies : dep)
test('prototest', e)
# Builds proto trees with generators. This acturally looks cleaner than the custom_target
# approach, but with some limits: it requires that the current source dir to be treated as
# proto root, instead of the top level of the proto tree. This may require changes of existing
# code.
proto_gen = generator(protoc,
arguments: ['-I', '@CURRENT_SOURCE_DIR@', '--cpp_out', '@BUILD_DIR@', '@INPUT@'],
output: ['@[email protected]', '@[email protected]'],
)
protos_name = [
'proto_generator/messages/dummy_messages3.proto',
'proto_generator/dummy_service3.proto',
]
protos_gen_src = proto_gen.process(protos_name)

subdir('asubdir')
dummy_bin = executable(
'prog',
['src/prog.cpp', protos_src, protos_hdr, protos_subdir_src, protos_subdir_hdr, protos_gen_src],
dependencies: protobuf_dep,
)
Loading