diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index 7372c4c69..d6f1c3877 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -603,19 +603,15 @@ class Backend: return srcs def eval_custom_target_command(self, target, absolute_outputs=False): - # We only want the outputs to be absolute when using the VS backend - if not absolute_outputs: - ofilenames = [os.path.join(self.get_target_dir(target), i) for i in target.output] - else: - ofilenames = [os.path.join(self.environment.get_build_dir(), self.get_target_dir(target), i) - for i in target.output] - srcs = self.get_custom_target_sources(target) + # We want the outputs to be absolute only when using the VS backend outdir = self.get_target_dir(target) - # Many external programs fail on empty arguments. - if outdir == '': - outdir = '.' - if target.absolute_paths: + if absolute_outputs: outdir = os.path.join(self.environment.get_build_dir(), outdir) + outputs = [] + for i in target.output: + outputs.append(os.path.join(outdir, i)) + inputs = self.get_custom_target_sources(target) + # Evaluate the command list cmd = [] for i in target.command: if isinstance(i, build.Executable): @@ -631,37 +627,10 @@ class Backend: if target.absolute_paths: i = os.path.join(self.environment.get_build_dir(), i) # FIXME: str types are blindly added ignoring 'target.absolute_paths' + # because we can't know if they refer to a file or just a string elif not isinstance(i, str): err_msg = 'Argument {0} is of unknown type {1}' raise RuntimeError(err_msg.format(str(i), str(type(i)))) - for (j, src) in enumerate(srcs): - i = i.replace('@INPUT%d@' % j, src) - for (j, res) in enumerate(ofilenames): - i = i.replace('@OUTPUT%d@' % j, res) - if '@INPUT@' in i: - msg = 'Custom target {} has @INPUT@ in the command, but'.format(target.name) - if len(srcs) == 0: - raise MesonException(msg + ' no input files') - if i == '@INPUT@': - cmd += srcs - continue - else: - if len(srcs) > 1: - raise MesonException(msg + ' more than one input file') - i = i.replace('@INPUT@', srcs[0]) - elif '@OUTPUT@' in i: - msg = 'Custom target {} has @OUTPUT@ in the command, but'.format(target.name) - if len(ofilenames) == 0: - raise MesonException(msg + ' no output files') - if i == '@OUTPUT@': - cmd += ofilenames - continue - else: - if len(ofilenames) > 1: - raise MesonException(msg + ' more than one output file') - i = i.replace('@OUTPUT@', ofilenames[0]) - elif '@OUTDIR@' in i: - i = i.replace('@OUTDIR@', outdir) elif '@DEPFILE@' in i: if target.depfile is None: msg = 'Custom target {!r} has @DEPFILE@ but no depfile ' \ @@ -680,10 +649,11 @@ class Backend: lead_dir = '' else: lead_dir = self.environment.get_build_dir() - i = i.replace(source, - os.path.join(lead_dir, - outdir)) + i = i.replace(source, os.path.join(lead_dir, outdir)) cmd.append(i) + # Substitute the rest of the template strings + values = mesonlib.get_filenames_templates_dict(inputs, outputs) + cmd = mesonlib.substitute_values(cmd, values) # This should not be necessary but removing it breaks # building GStreamer on Windows. The underlying issue # is problems with quoting backslashes on Windows @@ -703,7 +673,7 @@ class Backend: # # https://github.com/mesonbuild/meson/pull/737 cmd = [i.replace('\\', '/') for i in cmd] - return srcs, ofilenames, cmd + return inputs, outputs, cmd def run_postconf_scripts(self): env = {'MESON_SOURCE_ROOT': self.environment.get_source_dir(), diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 5f2de3bdb..91a3dd81f 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -16,7 +16,9 @@ from . import environment from . import dependencies from . import mlog import copy, os, re -from .mesonlib import File, flatten, MesonException, stringlistify, classify_unity_sources +from .mesonlib import File, MesonException +from .mesonlib import flatten, stringlistify, classify_unity_sources +from .mesonlib import get_filenames_templates_dict, substitute_values from .environment import for_windows, for_darwin from .compilers import is_object, clike_langs, lang_suffixes @@ -1331,11 +1333,25 @@ class CustomTarget(Target): self.output = kwargs['output'] if not isinstance(self.output, list): self.output = [self.output] + # This will substitute values from the input into output and return it. + inputs = get_sources_string_names(self.sources) + values = get_filenames_templates_dict(inputs, []) for i in self.output: 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 '@INPUT@' in i or '@INPUT0@' in i: + m = 'Output cannot contain @INPUT@ or @INPUT0@, did you ' \ + 'mean @PLAINNAME@ or @BASENAME@?' + raise InvalidArguments(m) + # We already check this during substitution, but the error message + # will be unclear/confusing, so check it here. + if len(inputs) != 1 and ('@PLAINNAME@' in i or '@BASENAME@' in i): + m = "Output cannot contain @PLAINNAME@ or @BASENAME@ when " \ + "there is more than one input (we can't know which to use)" + raise InvalidArguments(m) + self.output = substitute_values(self.output, values) self.capture = kwargs.get('capture', False) if self.capture and len(self.output) != 1: raise InvalidArguments('Capturing can only output to a single file.') @@ -1530,3 +1546,22 @@ class TestSetup: self.gdb = gdb self.timeout_multiplier = timeout_multiplier self.env = env + +def get_sources_string_names(sources): + ''' + For the specified list of @sources which can be strings, Files, or targets, + get all the output basenames. + ''' + names = [] + for s in sources: + if hasattr(s, 'held_object'): + s = s.held_object + if isinstance(s, str): + names.append(s) + elif isinstance(s, (BuildTarget, CustomTarget, GeneratedList)): + names += s.get_outputs() + elif isinstance(s, File): + names.append(s.fname) + else: + raise AssertionError('Unknown source type: {!r}'.format(s)) + return names diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index f6065d5a0..0d252fd1b 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -2224,12 +2224,28 @@ requirements use the version keyword argument instead.''') raise InterpreterException("configure_file takes only keyword arguments.") if 'output' not in kwargs: raise InterpreterException('Required keyword argument "output" not defined.') - inputfile = kwargs.get('input', None) + if 'configuration' in kwargs and 'command' in kwargs: + raise InterpreterException('Must not specify both "configuration" ' + 'and "command" keyword arguments since ' + 'they are mutually exclusive.') + # Validate input + inputfile = None + if 'input' in kwargs: + inputfile = kwargs['input'] + if isinstance(inputfile, list): + if len(inputfile) != 1: + m = "Keyword argument 'input' requires exactly one file" + raise InterpreterException(m) + inputfile = inputfile[0] + if not isinstance(inputfile, (str, mesonlib.File)): + raise InterpreterException('Input must be a string or a file') + ifile_abs = os.path.join(self.environment.source_dir, self.subdir, inputfile) + elif 'command' in kwargs: + raise InterpreterException('Required keyword argument \'input\' missing') + # Validate output output = kwargs['output'] - if not isinstance(inputfile, (str, type(None))): - raise InterpreterException('Input must be a string.') if not isinstance(output, str): - raise InterpreterException('Output must be a string.') + raise InterpreterException('Output file name must be a string') if os.path.split(output)[0] != '': raise InterpreterException('Output file name must not contain a subdirectory.') (ofile_path, ofile_fname) = os.path.split(os.path.join(self.subdir, output)) @@ -2238,6 +2254,7 @@ requirements use the version keyword argument instead.''') 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: # Normalize the path of the conffile to avoid duplicates # This is especially important to convert '/' to '\' on Windows @@ -2245,15 +2262,19 @@ requirements use the version keyword argument instead.''') if conffile not in self.build_def_files: self.build_def_files.append(conffile) os.makedirs(os.path.join(self.environment.build_dir, self.subdir), exist_ok=True) - ifile_abs = os.path.join(self.environment.source_dir, self.subdir, inputfile) mesonlib.do_conf_file(ifile_abs, ofile_abs, conf.held_object) else: mesonlib.dump_conf_header(ofile_abs, conf.held_object) conf.mark_used() elif 'command' in kwargs: - if 'input' not in kwargs: - raise InterpreterException('Required keyword input missing.') - res = self.func_run_command(node, kwargs['command'], {}) + # We use absolute paths for input and output here because the cwd + # that the command is run from is 'unspecified', so it could change. + # Currently it's builddir/subdir for in_builddir else srcdir/subdir. + values = mesonlib.get_filenames_templates_dict([ifile_abs], [ofile_abs]) + # Substitute @INPUT@, @OUTPUT@, etc here. + cmd = mesonlib.substitute_values(kwargs['command'], values) + mlog.log('Configuring', mlog.bold(output), 'with command') + res = self.func_run_command(node, cmd, {'in_builddir': True}) if res.returncode != 0: raise InterpreterException('Running configure command failed.\n%s\n%s' % (res.stdout, res.stderr)) diff --git a/mesonbuild/mesonlib.py b/mesonbuild/mesonlib.py index f0b20e1bd..c7368d577 100644 --- a/mesonbuild/mesonlib.py +++ b/mesonbuild/mesonlib.py @@ -521,3 +521,154 @@ def commonpath(paths): new = os.path.join(*new) common = pathlib.PurePath(new) return str(common) + +def iter_regexin_iter(regexiter, initer): + ''' + Takes each regular expression in @regexiter and tries to search for it in + every item in @initer. If there is a match, returns that match. + Else returns False. + ''' + for regex in regexiter: + for ii in initer: + if not isinstance(ii, str): + continue + match = re.search(regex, ii) + if match: + return match.group() + return False + +def _substitute_values_check_errors(command, values): + # Error checking + inregex = ('@INPUT([0-9]+)?@', '@PLAINNAME@', '@BASENAME@') + outregex = ('@OUTPUT([0-9]+)?@', '@OUTDIR@') + if '@INPUT@' not in values: + # Error out if any input-derived templates are present in the command + match = iter_regexin_iter(inregex, command) + if match: + m = 'Command cannot have {!r}, since no input files were specified' + raise MesonException(m.format(match)) + else: + if len(values['@INPUT@']) > 1: + # Error out if @PLAINNAME@ or @BASENAME@ is present in the command + match = iter_regexin_iter(inregex[1:], command) + if match: + raise MesonException('Command cannot have {!r} when there is ' + 'more than one input file'.format(match)) + # Error out if an invalid @INPUTnn@ template was specified + for each in command: + if not isinstance(each, str): + continue + match = re.search(inregex[0], each) + if match and match.group() not in values: + m = 'Command cannot have {!r} since there are only {!r} inputs' + raise MesonException(m.format(match.group(), len(values['@INPUT@']))) + if '@OUTPUT@' not in values: + # Error out if any output-derived templates are present in the command + match = iter_regexin_iter(outregex, command) + if match: + m = 'Command cannot have {!r} since there are no outputs' + raise MesonException(m.format(match)) + else: + # Error out if an invalid @OUTPUTnn@ template was specified + for each in command: + if not isinstance(each, str): + continue + match = re.search(outregex[0], each) + if match and match.group() not in values: + m = 'Command cannot have {!r} since there are only {!r} outputs' + raise MesonException(m.format(match.group(), len(values['@OUTPUT@']))) + +def substitute_values(command, values): + ''' + Substitute the template strings in the @values dict into the list of + strings @command and return a new list. For a full list of the templates, + see get_filenames_templates_dict() + + If multiple inputs/outputs are given in the @values dictionary, we + substitute @INPUT@ and @OUTPUT@ only if they are the entire string, not + just a part of it, and in that case we substitute *all* of them. + ''' + # Error checking + _substitute_values_check_errors(command, values) + # Substitution + outcmd = [] + for vv in command: + if not isinstance(vv, str): + outcmd.append(vv) + elif '@INPUT@' in vv: + inputs = values['@INPUT@'] + if vv == '@INPUT@': + outcmd += inputs + elif len(inputs) == 1: + outcmd.append(vv.replace('@INPUT@', inputs[0])) + else: + raise MesonException("Command has '@INPUT@' as part of a " + "string and more than one input file") + elif '@OUTPUT@' in vv: + outputs = values['@OUTPUT@'] + if vv == '@OUTPUT@': + outcmd += outputs + elif len(outputs) == 1: + outcmd.append(vv.replace('@OUTPUT@', outputs[0])) + else: + raise MesonException("Command has '@OUTPUT@' as part of a " + "string and more than one output file") + # Append values that are exactly a template string. + # This is faster than a string replace. + elif vv in values: + outcmd.append(values[vv]) + # Substitute everything else with replacement + else: + for key, value in values.items(): + if key in ('@INPUT@', '@OUTPUT@'): + # Already done above + continue + vv = vv.replace(key, value) + outcmd.append(vv) + return outcmd + +def get_filenames_templates_dict(inputs, outputs): + ''' + Create a dictionary with template strings as keys and values as values for + the following templates: + + @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 + + If there is more than one input file, the following keys are also created: + + @INPUT0@, @INPUT1@, ... one for each input file + + If there is more than one output file, the following keys are also created: + + @OUTPUT0@, @OUTPUT1@, ... one for each output file + ''' + values = {} + # Gather values derived from the input + if inputs: + # We want to substitute all the inputs. + values['@INPUT@'] = inputs + for (ii, vv) in enumerate(inputs): + # Write out @INPUT0@, @INPUT1@, ... + values['@INPUT{}@'.format(ii)] = vv + if len(inputs) == 1: + # Just one value, substitute @PLAINNAME@ and @BASENAME@ + values['@PLAINNAME@'] = plain = os.path.split(inputs[0])[1] + values['@BASENAME@'] = os.path.splitext(plain)[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 diff --git a/mesonbuild/modules/gnome.py b/mesonbuild/modules/gnome.py index f288b58b7..bf4b8bee7 100644 --- a/mesonbuild/modules/gnome.py +++ b/mesonbuild/modules/gnome.py @@ -115,11 +115,24 @@ class GnomeModule(ExtensionModule): ifile = args[1] if isinstance(ifile, mesonlib.File): - ifile = os.path.join(ifile.subdir, ifile.fname) + # glib-compile-resources will be run inside the source dir, + # so we need either 'src_to_build' or the absolute path. + # Absolute path is the easiest choice. + if ifile.is_built: + ifile = os.path.join(state.environment.get_build_dir(), ifile.subdir, ifile.fname) + else: + ifile = os.path.join(ifile.subdir, ifile.fname) elif isinstance(ifile, str): ifile = os.path.join(state.subdir, ifile) + elif isinstance(ifile, (interpreter.CustomTargetHolder, + interpreter.GeneratedObjectsHolder)): + m = 'Resource xml files generated at build-time cannot be used ' \ + 'with gnome.compile_resources() because we need to scan ' \ + 'the xml for dependencies. Use configure_file() instead ' \ + 'to generate it at configure-time.' + raise MesonException(m) else: - raise RuntimeError('Unreachable code.') + raise MesonException('Invalid file argument: {!r}'.format(ifile)) depend_files, depends, subdirs = self._get_gresource_dependencies( state, ifile, source_dirs, dependencies) @@ -202,9 +215,10 @@ class GnomeModule(ExtensionModule): cmd += ['--sourcedir', os.path.join(state.subdir, source_dir)] cmd += ['--sourcedir', state.subdir] # Current dir - pc, stdout = Popen_safe(cmd, cwd=state.environment.get_source_dir())[0:2] + pc, stdout, stderr = Popen_safe(cmd, cwd=state.environment.get_source_dir()) if pc.returncode != 0: - mlog.warning('glib-compile-resources has failed to get the dependencies for {}'.format(cmd[1])) + m = 'glib-compile-resources failed to get dependencies for {}:\n{}' + mlog.warning(m.format(cmd[1], stderr)) raise subprocess.CalledProcessError(pc.returncode, cmd) dep_files = stdout.split('\n')[:-1] @@ -866,6 +880,7 @@ class GnomeModule(ExtensionModule): } custom_kwargs.update(kwargs) return build.CustomTarget(output, state.subdir, custom_kwargs, + # https://github.com/mesonbuild/meson/issues/973 absolute_paths=True) def genmarshal(self, state, args, kwargs): diff --git a/run_tests.py b/run_tests.py index f2038e4cb..5025057ab 100755 --- a/run_tests.py +++ b/run_tests.py @@ -14,12 +14,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -import subprocess, sys, shutil +import os +import sys +import shutil +import subprocess import platform from mesonbuild import mesonlib if __name__ == '__main__': returncode = 0 + # Running on a developer machine? Be nice! + if not mesonlib.is_windows() and 'TRAVIS' not in os.environ: + os.nice(20) print('Running unittests.\n') units = ['InternalTests', 'AllPlatformTests'] if mesonlib.is_linux(): diff --git a/run_unittests.py b/run_unittests.py index 95e52e3b1..8c8844d4c 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -180,6 +180,157 @@ class InternalTests(unittest.TestCase): libdir = '/some/path/to/prefix/libdir' self.assertEqual(commonpath([prefix, libdir]), str(PurePath(prefix))) + def test_string_templates_substitution(self): + dictfunc = mesonbuild.mesonlib.get_filenames_templates_dict + substfunc = mesonbuild.mesonlib.substitute_values + ME = mesonbuild.mesonlib.MesonException + + # Identity + self.assertEqual(dictfunc([], []), {}) + + # One input, no outputs + inputs = ['bar/foo.c.in'] + outputs = [] + ret = dictfunc(inputs, outputs) + d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], + '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c'} + # Check dictionary + self.assertEqual(ret, d) + # Check substitutions + cmd = ['some', 'ordinary', 'strings'] + self.assertEqual(substfunc(cmd, d), cmd) + cmd = ['@INPUT@.out', 'ordinary', 'strings'] + self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out'] + cmd[1:]) + cmd = ['@INPUT0@.out', '@PLAINNAME@.ok', 'strings'] + self.assertEqual(substfunc(cmd, d), + [inputs[0] + '.out'] + [d['@PLAINNAME@'] + '.ok'] + cmd[2:]) + cmd = ['@INPUT@', '@BASENAME@.hah', 'strings'] + self.assertEqual(substfunc(cmd, d), + inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:]) + cmd = ['@OUTPUT@'] + self.assertRaises(ME, substfunc, cmd, d) + + # One input, one output + inputs = ['bar/foo.c.in'] + 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@': '.'} + # Check dictionary + self.assertEqual(ret, d) + # Check substitutions + cmd = ['some', 'ordinary', 'strings'] + self.assertEqual(substfunc(cmd, d), cmd) + cmd = ['@INPUT@.out', '@OUTPUT@', 'strings'] + self.assertEqual(substfunc(cmd, d), + [inputs[0] + '.out'] + outputs + cmd[2:]) + cmd = ['@INPUT0@.out', '@PLAINNAME@.ok', '@OUTPUT0@'] + self.assertEqual(substfunc(cmd, d), + [inputs[0] + '.out', d['@PLAINNAME@'] + '.ok'] + outputs) + cmd = ['@INPUT@', '@BASENAME@.hah', 'strings'] + self.assertEqual(substfunc(cmd, d), + inputs + [d['@BASENAME@'] + '.hah'] + 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'} + # Check dictionary + self.assertEqual(ret, d) + + # Two inputs, no outputs + inputs = ['bar/foo.c.in', 'baz/foo.c.in'] + outputs = [] + ret = dictfunc(inputs, outputs) + d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1]} + # Check dictionary + self.assertEqual(ret, d) + # Check substitutions + cmd = ['some', 'ordinary', 'strings'] + self.assertEqual(substfunc(cmd, d), cmd) + cmd = ['@INPUT@', 'ordinary', 'strings'] + self.assertEqual(substfunc(cmd, d), inputs + cmd[1:]) + cmd = ['@INPUT0@.out', 'ordinary', 'strings'] + self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out'] + cmd[1:]) + cmd = ['@INPUT0@.out', '@INPUT1@.ok', 'strings'] + self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out', inputs[1] + '.ok'] + cmd[2:]) + cmd = ['@INPUT0@', '@INPUT1@', 'strings'] + self.assertEqual(substfunc(cmd, d), inputs + cmd[2:]) + # Many inputs, can't use @INPUT@ like this + cmd = ['@INPUT@.out', 'ordinary', 'strings'] + # Not enough inputs + cmd = ['@INPUT2@.out', 'ordinary', 'strings'] + self.assertRaises(ME, substfunc, cmd, d) + # Too many inputs + cmd = ['@PLAINNAME@'] + self.assertRaises(ME, substfunc, cmd, d) + cmd = ['@BASENAME@'] + 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'} + # Check dictionary + self.assertEqual(ret, d) + # Check substitutions + cmd = ['some', 'ordinary', 'strings'] + self.assertEqual(substfunc(cmd, d), cmd) + cmd = ['@OUTPUT@', 'ordinary', 'strings'] + self.assertEqual(substfunc(cmd, d), outputs + cmd[1:]) + cmd = ['@OUTPUT@.out', 'ordinary', 'strings'] + self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out'] + cmd[1:]) + cmd = ['@OUTPUT0@.out', '@INPUT1@.ok', 'strings'] + self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok'] + cmd[2:]) + # Many inputs, can't use @INPUT@ like this + cmd = ['@INPUT@.out', 'ordinary', 'strings'] + # Not enough inputs + cmd = ['@INPUT2@.out', 'ordinary', 'strings'] + self.assertRaises(ME, substfunc, cmd, d) + # Not enough outputs + cmd = ['@OUTPUT2@.out', 'ordinary', 'strings'] + self.assertRaises(ME, substfunc, cmd, d) + + # Two inputs, two outputs + 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'} + # Check dictionary + self.assertEqual(ret, d) + # Check substitutions + cmd = ['some', 'ordinary', 'strings'] + self.assertEqual(substfunc(cmd, d), cmd) + cmd = ['@OUTPUT@', 'ordinary', 'strings'] + self.assertEqual(substfunc(cmd, d), outputs + cmd[1:]) + cmd = ['@OUTPUT0@', '@OUTPUT1@', 'strings'] + self.assertEqual(substfunc(cmd, d), outputs + cmd[2:]) + cmd = ['@OUTPUT0@.out', '@INPUT1@.ok', '@OUTDIR@'] + self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok', 'dir']) + # Many inputs, can't use @INPUT@ like this + cmd = ['@INPUT@.out', 'ordinary', 'strings'] + # Not enough inputs + cmd = ['@INPUT2@.out', 'ordinary', 'strings'] + self.assertRaises(ME, substfunc, cmd, d) + # Not enough outputs + cmd = ['@OUTPUT2@.out', 'ordinary', 'strings'] + self.assertRaises(ME, substfunc, cmd, d) + # Many outputs, can't use @OUTPUT@ like this + cmd = ['@OUTPUT@.out', 'ordinary', 'strings'] + self.assertRaises(ME, substfunc, cmd, d) + class BasePlatformTests(unittest.TestCase): def setUp(self): diff --git a/test cases/common/118 allgenerate/meson.build b/test cases/common/118 allgenerate/meson.build index 36abbe966..049e8498c 100644 --- a/test cases/common/118 allgenerate/meson.build +++ b/test cases/common/118 allgenerate/meson.build @@ -13,7 +13,7 @@ c = g.process('foobar.cpp.in') prog = executable('genexe', c) c2 = custom_target('c2gen', - output : 'c2gen.cpp', + output : '@BASENAME@', input : 'foobar.cpp.in', command : [comp, '@INPUT@', '@OUTPUT@']) diff --git a/test cases/common/129 object only target/meson.build b/test cases/common/129 object only target/meson.build index 58d01d930..d83a65825 100644 --- a/test cases/common/129 object only target/meson.build +++ b/test cases/common/129 object only target/meson.build @@ -16,7 +16,7 @@ cc = meson.get_compiler('c').cmd_array().get(-1) # provided by the source tree source1 = configure_file(input : 'source.c', output : 'source' + ext, - command : [comp, cc, 'source.c', + command : [comp, cc, files('source.c'), join_paths(meson.current_build_dir(), 'source' + ext)]) obj = static_library('obj', objects : source1) diff --git a/test cases/common/16 configure file/check_file.py b/test cases/common/16 configure file/check_file.py new file mode 100644 index 000000000..449b77a8f --- /dev/null +++ b/test cases/common/16 configure file/check_file.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import os +import sys + +assert(os.path.exists(sys.argv[1])) diff --git a/test cases/common/16 configure file/generator.py b/test cases/common/16 configure file/generator.py index 2c7f2f845..e3cc88101 100755 --- a/test cases/common/16 configure file/generator.py +++ b/test cases/common/16 configure file/generator.py @@ -1,15 +1,17 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 -# On some platforms "python" points to Python 2 -# on others to Python 3. Work with both. - -from __future__ import print_function import sys, os +from pathlib import Path if len(sys.argv) != 3: print("Wrong amount of parameters.") -assert(os.path.exists(sys.argv[1])) +build_dir = Path(os.environ['MESON_BUILD_ROOT']) +subdir = Path(os.environ['MESON_SUBDIR']) +inputf = Path(sys.argv[1]) +outputf = Path(sys.argv[2]) + +assert(inputf.exists()) -with open(sys.argv[2], 'w') as ofile: +with outputf.open('w') as ofile: ofile.write("#define ZERO_RESULT 0\n") diff --git a/test cases/common/16 configure file/installed_files.txt b/test cases/common/16 configure file/installed_files.txt index 219b4c09b..d9fee1277 100644 --- a/test cases/common/16 configure file/installed_files.txt +++ b/test cases/common/16 configure file/installed_files.txt @@ -1 +1,3 @@ usr/share/appdir/config2.h +usr/share/appdireh/config2-1.h +usr/share/appdirok/config2-2.h diff --git a/test cases/common/16 configure file/meson.build b/test cases/common/16 configure file/meson.build index b764c5ade..bff041b96 100644 --- a/test cases/common/16 configure file/meson.build +++ b/test cases/common/16 configure file/meson.build @@ -23,18 +23,22 @@ cfile) test('inctest', e) # Now generate a header file with an external script. -genprog = find_program('python3', required : false) -if not genprog.found() - genprog = find_program('python') -endif +genprog = import('python3').find_python() scriptfile = '@0@/generator.py'.format(meson.current_source_dir()) ifile = '@0@/dummy.dat'.format(meson.current_source_dir()) ofile = '@0@/config2.h'.format(meson.current_build_dir()) +check_file = find_program('check_file.py') +# Configure in source root with command and absolute paths configure_file(input : 'dummy.dat', -output : 'config2.h', -command : [genprog, scriptfile, ifile, ofile], -install_dir : 'share/appdir') + output : 'config2.h', + command : [genprog, scriptfile, ifile, ofile], + install_dir : 'share/appdir') +run_command(check_file, join_paths(meson.current_build_dir(), 'config2.h')) + +found_script = find_program('generator.py') +# More configure_file tests in here +subdir('subdir') test('inctest2', executable('prog2', 'prog2.c')) diff --git a/test cases/common/16 configure file/subdir/meson.build b/test cases/common/16 configure file/subdir/meson.build new file mode 100644 index 000000000..d802c1d86 --- /dev/null +++ b/test cases/common/16 configure file/subdir/meson.build @@ -0,0 +1,19 @@ +# Configure in subdir with absolute paths for input and relative for output +configure_file(input : '../dummy.dat', + output : 'config2-1.h', + command : [genprog, scriptfile, ifile, 'config2-1.h'], + install_dir : 'share/appdireh') +run_command(check_file, join_paths(meson.current_build_dir(), 'config2-1.h')) + +# Configure in subdir with files() for input and relative for output +configure_file(input : '../dummy.dat', + output : 'config2-2.h', + command : [genprog, scriptfile, files('../dummy.dat'), 'config2-2.h'], + install_dir : 'share/appdirok') +run_command(check_file, join_paths(meson.current_build_dir(), 'config2-2.h')) + +# Configure in subdir with string templates for input and output +configure_file(input : '../dummy.dat', + output : 'config2-3.h', + command : [found_script, '@INPUT@', '@OUTPUT@']) +run_command(check_file, join_paths(meson.current_build_dir(), 'config2-3.h')) diff --git a/test cases/failing/42 custom target plainname many inputs/1.txt b/test cases/failing/42 custom target plainname many inputs/1.txt new file mode 100644 index 000000000..d00491fd7 --- /dev/null +++ b/test cases/failing/42 custom target plainname many inputs/1.txt @@ -0,0 +1 @@ +1 diff --git a/test cases/failing/42 custom target plainname many inputs/2.txt b/test cases/failing/42 custom target plainname many inputs/2.txt new file mode 100644 index 000000000..0cfbf0888 --- /dev/null +++ b/test cases/failing/42 custom target plainname many inputs/2.txt @@ -0,0 +1 @@ +2 diff --git a/test cases/failing/42 custom target plainname many inputs/catfiles.py b/test cases/failing/42 custom target plainname many inputs/catfiles.py new file mode 100644 index 000000000..1c53e24e7 --- /dev/null +++ b/test cases/failing/42 custom target plainname many inputs/catfiles.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +import sys + +out = sys.argv[-1] +with open(out, 'wb') as o: + for infile in sys.argv[1:-1]: + with open(infile, 'rb') as f: + o.write(f.read()) diff --git a/test cases/failing/42 custom target plainname many inputs/meson.build b/test cases/failing/42 custom target plainname many inputs/meson.build new file mode 100644 index 000000000..1bcfc0672 --- /dev/null +++ b/test cases/failing/42 custom target plainname many inputs/meson.build @@ -0,0 +1,8 @@ +project('plain name many inputs', 'c') + +catfiles = find_program('catfiles.py') + +custom_target('plainname-inputs', + input : ['1.txt', '2.txt'], + output : '@PLAINNAME@.dat', + command : [catfiles, '@INPUT@', '@OUTPUT@']) diff --git a/test cases/frameworks/7 gnome/resources/copyfile.py b/test cases/frameworks/7 gnome/resources/copyfile.py new file mode 100644 index 000000000..7e44c48dd --- /dev/null +++ b/test cases/frameworks/7 gnome/resources/copyfile.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import sys +import shutil + +shutil.copy(sys.argv[1], sys.argv[2]) diff --git a/test cases/frameworks/7 gnome/resources/meson.build b/test cases/frameworks/7 gnome/resources/meson.build index 2e7250134..fdf6f6332 100644 --- a/test cases/frameworks/7 gnome/resources/meson.build +++ b/test cases/frameworks/7 gnome/resources/meson.build @@ -1,8 +1,15 @@ # There are two tests here, because the 2nd one depends on a version of -# GLib (2.48.2) that is very recent at the time of writing. +# GLib (2.51.1) that is very recent at the time of writing. + +copyfile = find_program('copyfile.py') + +simple_gresource = configure_file( + input : 'simple.gresource.xml', + output : 'simple-gen.gresource.xml', + command : [copyfile, '@INPUT@', '@OUTPUT@']) simple_resources = gnome.compile_resources('simple-resources', - 'simple.gresource.xml', + simple_gresource, install_header : true, export : true, source_dir : '../resources-data',