aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/markdown/Reference-manual.md26
-rw-r--r--docs/markdown/Syntax.md67
-rw-r--r--docs/markdown/Users.md2
-rw-r--r--docs/markdown/snippets/dict_builtin.md19
-rw-r--r--docs/markdown/snippets/run_command_check.md4
-rw-r--r--mesonbuild/interpreter.py24
-rw-r--r--mesonbuild/interpreterbase.py98
-rw-r--r--mesonbuild/mparser.py60
-rwxr-xr-xrun_unittests.py14
-rw-r--r--test cases/common/199 dict/meson.build23
-rw-r--r--test cases/common/199 dict/prog.c8
-rw-r--r--test cases/failing/75 run_command unclean exit/meson.build4
-rwxr-xr-xtest cases/failing/75 run_command unclean exit/returncode.py4
13 files changed, 321 insertions, 32 deletions
diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md
index d875098..804ca0f 100644
--- a/docs/markdown/Reference-manual.md
+++ b/docs/markdown/Reference-manual.md
@@ -1022,7 +1022,7 @@ Project supports the following keyword arguments.
### run_command()
``` meson
- runresult run_command(command, list_of_args)
+ runresult run_command(command, list_of_args, ...)
```
Runs the command specified in positional arguments.
@@ -1037,6 +1037,13 @@ Meson will set three environment variables `MESON_SOURCE_ROOT`,
directory, build directory and subdirectory the target was defined in,
respectively.
+This function has one keyword argument.
+
+ - `check` takes a boolean. If `true`, the exit status code of the command will
+ be checked, and the configuration will fail if it is non-zero. The default is
+ `false`.
+ Since 0.47.0
+
See also [External commands](External-commands.md).
### run_target
@@ -1736,6 +1743,23 @@ The following methods are defined for all [arrays](Syntax.md#arrays):
You can also iterate over arrays with the [`foreach`
statement](Syntax.md#foreach-statements).
+### `dictionary` object
+
+The following methods are defined for all [dictionaries](Syntax.md#dictionaries):
+
+- `has_key(key)` returns `true` if the dictionary contains the key
+ given as argument, `false` otherwise
+
+- `get(key, fallback)`, returns the value for the key given as first argument
+ if it is present in the dictionary, or the optional fallback value given
+ as the second argument. If a single argument was given and the key was not
+ found, causes a fatal error
+
+You can also iterate over dictionaries with the [`foreach`
+statement](Syntax.md#foreach-statements).
+
+Dictionaries are available since 0.47.0.
+
## Returned objects
These are objects returned by the [functions listed above](#functions).
diff --git a/docs/markdown/Syntax.md b/docs/markdown/Syntax.md
index 30eedf8..42002f4 100644
--- a/docs/markdown/Syntax.md
+++ b/docs/markdown/Syntax.md
@@ -284,6 +284,31 @@ The following methods are defined for all arrays:
- `contains`, returns `true` if the array contains the object given as argument, `false` otherwise
- `get`, returns the object at the given index, negative indices count from the back of the array, indexing out of bounds is a fatal error. Provided for backwards-compatibility, it is identical to array indexing.
+Dictionaries
+--
+
+Dictionaries are delimited by curly braces. A dictionary can contain an
+arbitrary number of key value pairs. Keys are required to be literal
+strings, values can be objects of any type.
+
+```meson
+my_dict = {'foo': 42, 'bar': 'baz'}
+```
+
+Keys must be unique:
+
+```meson
+# This will fail
+my_dict = {'foo': 42, 'foo': 43}
+```
+
+Dictionaries are immutable.
+
+Dictionaries are available since 0.47.0.
+
+Visit the [Reference Manual](Reference-manual.md#dictionary-object) to read
+about the methods exposed by dictionaries.
+
Function calls
--
@@ -329,9 +354,17 @@ endif
## Foreach statements
-To do an operation on all elements of an array, use the `foreach`
-command. As an example, here's how you would define two executables
-with corresponding tests.
+To do an operation on all elements of an iterable, use the `foreach`
+command.
+
+> Note that Meson variables are immutable. Trying to assign a new value
+> to the iterated object inside a foreach loop will not affect foreach's
+> control flow.
+
+### Foreach with an array
+
+Here's an example of how you could define two executables
+with corresponding tests using arrays and foreach.
```meson
progs = [['prog1', ['prog1.c', 'foo.c']],
@@ -343,9 +376,31 @@ foreach p : progs
endforeach
```
-Note that Meson variables are immutable. Trying to assign a new value
-to `progs` inside a foreach loop will not affect foreach's control
-flow.
+### Foreach with a dictionary
+
+Here's an example of you could iterate a set of components that
+should be compiled in according to some configuration. This uses
+a [dictionary][dictionaries], which is available since 0.47.0.
+
+```meson
+components = {
+ 'foo': ['foo.c'],
+ 'bar': ['bar.c'],
+ 'baz:' ['baz.c'],
+}
+
+# compute a configuration based on system dependencies, custom logic
+conf = configuration_data()
+conf.set('USE_FOO', 1)
+
+# Determine the sources to compile
+sources_to_compile = []
+foreach name, sources : components
+ if conf.get('USE_@0@'.format(name.to_upper()), 0) == 1
+ sources_to_compile += sources
+ endif
+endforeach
+```
Logical operations
--
diff --git a/docs/markdown/Users.md b/docs/markdown/Users.md
index 0a30d9c..a15e5eb 100644
--- a/docs/markdown/Users.md
+++ b/docs/markdown/Users.md
@@ -47,6 +47,8 @@ listed in the [`meson` GitHub topic](https://github.com/topics/meson).
- [Libhttpseverywhere](https://git.gnome.org/browse/libhttpseverywhere), a library to enable httpseverywhere on any desktop app
- [Libosmscout](https://github.com/Framstag/libosmscout), a C++ library for offline map rendering, routing and location
lookup based on OpenStreetMap data
+ - [libspng](https://gitlab.com/randy408/libspng), a C library for reading and writing Portable Network Graphics (PNG)
+format files
- [Libva](https://github.com/intel/libva), an implementation for the VA (VIdeo Acceleration) API
- [Libzim](https://github.com/openzim/libzim), the reference implementation for the ZIM file format
- [Kiwix libraries](https://github.com/kiwix/kiwix-lib)
diff --git a/docs/markdown/snippets/dict_builtin.md b/docs/markdown/snippets/dict_builtin.md
new file mode 100644
index 0000000..1bd24ce
--- /dev/null
+++ b/docs/markdown/snippets/dict_builtin.md
@@ -0,0 +1,19 @@
+## New built-in object dictionary
+
+Meson dictionaries use a syntax similar to python's dictionaries,
+but have a narrower scope: they are immutable, keys can only
+be string literals, and initializing a dictionary with duplicate
+keys causes a fatal error.
+
+Example usage:
+
+```meson
+dict = {'foo': 42, 'bar': 'baz'}
+
+foo = dict.get('foo')
+foobar = dict.get('foobar', 'fallback-value')
+
+foreach key, value : dict
+ Do something with key and value
+endforeach
+```
diff --git a/docs/markdown/snippets/run_command_check.md b/docs/markdown/snippets/run_command_check.md
new file mode 100644
index 0000000..018456b
--- /dev/null
+++ b/docs/markdown/snippets/run_command_check.md
@@ -0,0 +1,4 @@
+## New 'check' keyword argument for the run_command function
+
+If `check` is `true`, then the configuration will fail if the command returns a
+non-zero exit status. The default value is `false` for compatibility reasons.
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
index 25747c9..64fc693 100644
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -45,6 +45,8 @@ permitted_method_kwargs = {
def stringifyUserArguments(args):
if isinstance(args, list):
return '[%s]' % ', '.join([stringifyUserArguments(x) for x in args])
+ elif isinstance(args, dict):
+ return '{%s}' % ', '.join(['%s : %s' % (stringifyUserArguments(k), stringifyUserArguments(v)) for k, v in args.items()])
elif isinstance(args, int):
return str(args)
elif isinstance(args, str):
@@ -92,18 +94,18 @@ class TryRunResultHolder(InterpreterObject):
class RunProcess(InterpreterObject):
- def __init__(self, cmd, args, source_dir, build_dir, subdir, mesonintrospect, in_builddir=False):
+ def __init__(self, cmd, args, source_dir, build_dir, subdir, mesonintrospect, in_builddir=False, check=False):
super().__init__()
if not isinstance(cmd, ExternalProgram):
raise AssertionError('BUG: RunProcess must be passed an ExternalProgram')
- pc, self.stdout, self.stderr = self.run_command(cmd, args, source_dir, build_dir, subdir, mesonintrospect, in_builddir)
+ pc, self.stdout, self.stderr = self.run_command(cmd, args, source_dir, build_dir, subdir, mesonintrospect, in_builddir, check)
self.returncode = pc.returncode
self.methods.update({'returncode': self.returncode_method,
'stdout': self.stdout_method,
'stderr': self.stderr_method,
})
- def run_command(self, cmd, args, source_dir, build_dir, subdir, mesonintrospect, in_builddir):
+ def run_command(self, cmd, args, source_dir, build_dir, subdir, mesonintrospect, in_builddir, check=False):
command_array = cmd.get_command() + args
env = {'MESON_SOURCE_ROOT': source_dir,
'MESON_BUILD_ROOT': build_dir,
@@ -124,6 +126,10 @@ class RunProcess(InterpreterObject):
mlog.debug('----stderr----')
mlog.debug(e)
mlog.debug('')
+
+ if check and p.returncode != 0:
+ raise InterpreterException('Command "{}" failed with status {}.'.format(' '.join(command_array), p.returncode))
+
return p, o, e
except FileNotFoundError:
raise InterpreterException('Could not execute command "%s".' % ' '.join(command_array))
@@ -1679,6 +1685,7 @@ permitted_kwargs = {'add_global_arguments': {'language'},
'install_subdir': {'exclude_files', 'exclude_directories', 'install_dir', 'install_mode', 'strip_directory'},
'jar': build.known_jar_kwargs,
'project': {'version', 'meson_version', 'default_options', 'license', 'subproject_dir'},
+ 'run_command': {'check'},
'run_target': {'command', 'depends'},
'shared_library': build.known_shlib_kwargs,
'shared_module': build.known_shmod_kwargs,
@@ -1957,7 +1964,7 @@ external dependencies (including libraries) must go to "dependencies".''')
if not isinstance(actual, wanted):
raise InvalidArguments('Incorrect argument type.')
- @noKwargs
+ @permittedKwargs(permitted_kwargs['run_command'])
def func_run_command(self, node, args, kwargs):
return self.run_command_impl(node, args, kwargs)
@@ -1968,6 +1975,11 @@ external dependencies (including libraries) must go to "dependencies".''')
cargs = args[1:]
srcdir = self.environment.get_source_dir()
builddir = self.environment.get_build_dir()
+
+ check = kwargs.get('check', False)
+ if not isinstance(check, bool):
+ raise InterpreterException('Check must be boolean.')
+
m = 'must be a string, or the output of find_program(), files() '\
'or configure_file(), or a compiler object; not {!r}'
if isinstance(cmd, ExternalProgramHolder):
@@ -2020,7 +2032,7 @@ external dependencies (including libraries) must go to "dependencies".''')
if a not in self.build_def_files:
self.build_def_files.append(a)
return RunProcess(cmd, expanded_args, srcdir, builddir, self.subdir,
- self.environment.get_build_command() + ['introspect'], in_builddir)
+ self.environment.get_build_command() + ['introspect'], in_builddir, check)
@stringArgs
def func_gettext(self, nodes, args, kwargs):
@@ -2273,6 +2285,8 @@ to directly access options of other subprojects.''')
arg = posargs[0]
if isinstance(arg, list):
argstr = stringifyUserArguments(arg)
+ elif isinstance(arg, dict):
+ argstr = stringifyUserArguments(arg)
elif isinstance(arg, str):
argstr = arg
elif isinstance(arg, int):
diff --git a/mesonbuild/interpreterbase.py b/mesonbuild/interpreterbase.py
index 60b0465..9f323d1 100644
--- a/mesonbuild/interpreterbase.py
+++ b/mesonbuild/interpreterbase.py
@@ -265,6 +265,8 @@ class InterpreterBase:
return self.evaluate_comparison(cur)
elif isinstance(cur, mparser.ArrayNode):
return self.evaluate_arraystatement(cur)
+ elif isinstance(cur, mparser.DictNode):
+ return self.evaluate_dictstatement(cur)
elif isinstance(cur, mparser.NumberNode):
return cur.value
elif isinstance(cur, mparser.AndNode):
@@ -296,6 +298,11 @@ class InterpreterBase:
raise InvalidCode('Keyword arguments are invalid in array construction.')
return arguments
+ def evaluate_dictstatement(self, cur):
+ (arguments, kwargs) = self.reduce_arguments(cur.args)
+ assert (not arguments)
+ return kwargs
+
def evaluate_notstatement(self, cur):
v = self.evaluate_statement(cur.value)
if not isinstance(v, bool):
@@ -444,15 +451,28 @@ The result of this is undefined and will become a hard error in a future Meson r
def evaluate_foreach(self, node):
assert(isinstance(node, mparser.ForeachClauseNode))
- varname = node.varname.value
items = self.evaluate_statement(node.items)
- if is_disabler(items):
- return items
- if not isinstance(items, list):
- raise InvalidArguments('Items of foreach loop is not an array')
- for item in items:
- self.set_variable(varname, item)
- self.evaluate_codeblock(node.block)
+
+ if isinstance(items, list):
+ if len(node.varnames) != 1:
+ raise InvalidArguments('Foreach on array does not unpack')
+ varname = node.varnames[0].value
+ if is_disabler(items):
+ return items
+ for item in items:
+ self.set_variable(varname, item)
+ self.evaluate_codeblock(node.block)
+ elif isinstance(items, dict):
+ if len(node.varnames) != 2:
+ raise InvalidArguments('Foreach on dict unpacks key and value')
+ if is_disabler(items):
+ return items
+ for key, value in items.items():
+ self.set_variable(node.varnames[0].value, key)
+ self.set_variable(node.varnames[1].value, value)
+ self.evaluate_codeblock(node.block)
+ else:
+ raise InvalidArguments('Items of foreach loop must be an array or a dict')
def evaluate_plusassign(self, node):
assert(isinstance(node, mparser.PlusAssignmentNode))
@@ -491,12 +511,21 @@ The result of this is undefined and will become a hard error in a future Meson r
raise InterpreterException(
'Tried to index an object that doesn\'t support indexing.')
index = self.evaluate_statement(node.index)
- if not isinstance(index, int):
- raise InterpreterException('Index value is not an integer.')
- try:
- return iobject[index]
- except IndexError:
- raise InterpreterException('Index %d out of bounds of array of size %d.' % (index, len(iobject)))
+
+ if isinstance(iobject, dict):
+ if not isinstance(index, str):
+ raise InterpreterException('Key is not a string')
+ try:
+ return iobject[index]
+ except KeyError:
+ raise InterpreterException('Key %s is not in dict' % index)
+ else:
+ if not isinstance(index, int):
+ raise InterpreterException('Index value is not an integer.')
+ try:
+ return iobject[index]
+ except IndexError:
+ raise InterpreterException('Index %d out of bounds of array of size %d.' % (index, len(iobject)))
def function_call(self, node):
func_name = node.func_name
@@ -529,6 +558,8 @@ The result of this is undefined and will become a hard error in a future Meson r
return self.int_method_call(obj, method_name, args)
if isinstance(obj, list):
return self.array_method_call(obj, method_name, args)
+ if isinstance(obj, dict):
+ return self.dict_method_call(obj, method_name, args)
if isinstance(obj, mesonlib.File):
raise InvalidArguments('File object "%s" is not callable.' % obj)
if not isinstance(obj, InterpreterObject):
@@ -687,6 +718,43 @@ The result of this is undefined and will become a hard error in a future Meson r
m = 'Arrays do not have a method called {!r}.'
raise InterpreterException(m.format(method_name))
+ def dict_method_call(self, obj, method_name, args):
+ (posargs, kwargs) = self.reduce_arguments(args)
+ if is_disabled(posargs, kwargs):
+ return Disabler()
+
+ if method_name in ('has_key', 'get'):
+ if method_name == 'has_key':
+ if len(posargs) != 1:
+ raise InterpreterException('has_key() takes exactly one argument.')
+ else:
+ if len(posargs) not in (1, 2):
+ raise InterpreterException('get() takes one or two arguments.')
+
+ key = posargs[0]
+ if not isinstance(key, (str)):
+ raise InvalidArguments('Dictionary key must be a string.')
+
+ has_key = key in obj
+
+ if method_name == 'has_key':
+ return has_key
+
+ if has_key:
+ return obj[key]
+
+ if len(posargs) == 2:
+ return posargs[1]
+
+ raise InterpreterException('Key {!r} is not in the dictionary.'.format(key))
+
+ if method_name == 'keys':
+ if len(posargs) != 0:
+ raise InterpreterException('keys() takes no arguments.')
+ return list(obj.keys())
+
+ raise InterpreterException('Dictionaries do not have a method called "%s".' % method_name)
+
def reduce_arguments(self, args):
assert(isinstance(args, mparser.ArgumentNode))
if args.incorrect_order():
@@ -741,7 +809,7 @@ To specify a keyword argument, use : instead of =.''')
def is_assignable(self, value):
return isinstance(value, (InterpreterObject, dependencies.Dependency,
- str, int, list, mesonlib.File))
+ str, int, list, dict, mesonlib.File))
def is_elementary_type(self, v):
return isinstance(v, (int, float, str, bool, list))
diff --git a/mesonbuild/mparser.py b/mesonbuild/mparser.py
index 8cef377..78683be 100644
--- a/mesonbuild/mparser.py
+++ b/mesonbuild/mparser.py
@@ -104,6 +104,8 @@ class Lexer:
('rparen', re.compile(r'\)')),
('lbracket', re.compile(r'\[')),
('rbracket', re.compile(r'\]')),
+ ('lcurl', re.compile(r'\{')),
+ ('rcurl', re.compile(r'\}')),
('dblquote', re.compile(r'"')),
('string', re.compile(r"'([^'\\]|(\\.))*'")),
('comma', re.compile(r',')),
@@ -134,6 +136,7 @@ class Lexer:
loc = 0
par_count = 0
bracket_count = 0
+ curl_count = 0
col = 0
while loc < len(self.code):
matched = False
@@ -160,6 +163,10 @@ class Lexer:
bracket_count += 1
elif tid == 'rbracket':
bracket_count -= 1
+ elif tid == 'lcurl':
+ curl_count += 1
+ elif tid == 'rcurl':
+ curl_count -= 1
elif tid == 'dblquote':
raise ParseException('Double quotes are not supported. Use single quotes.', self.getline(line_start), lineno, col)
elif tid == 'string':
@@ -187,7 +194,7 @@ This will become a hard error in a future Meson release.""", self.getline(line_s
elif tid == 'eol' or tid == 'eol_cont':
lineno += 1
line_start = loc
- if par_count > 0 or bracket_count > 0:
+ if par_count > 0 or bracket_count > 0 or curl_count > 0:
break
elif tid == 'id':
if match_text in self.keywords:
@@ -241,6 +248,13 @@ class ArrayNode:
self.colno = args.colno
self.args = args
+class DictNode:
+ def __init__(self, args):
+ self.subdir = args.subdir
+ self.lineno = args.lineno
+ self.colno = args.colno
+ self.args = args
+
class EmptyNode:
def __init__(self, lineno, colno):
self.subdir = ''
@@ -340,10 +354,10 @@ class PlusAssignmentNode:
self.value = value
class ForeachClauseNode:
- def __init__(self, lineno, colno, varname, items, block):
+ def __init__(self, lineno, colno, varnames, items, block):
self.lineno = lineno
self.colno = colno
- self.varname = varname
+ self.varnames = varnames
self.items = items
self.block = block
@@ -601,6 +615,10 @@ class Parser:
args = self.args()
self.block_expect('rbracket', block_start)
return ArrayNode(args)
+ elif self.accept('lcurl'):
+ key_values = self.key_values()
+ self.block_expect('rcurl', block_start)
+ return DictNode(key_values)
else:
return self.e9()
@@ -618,6 +636,31 @@ class Parser:
return StringNode(t)
return EmptyNode(self.current.lineno, self.current.colno)
+ def key_values(self):
+ s = self.statement()
+ a = ArgumentNode(s)
+
+ while not isinstance(s, EmptyNode):
+ potential = self.current
+ if self.accept('colon'):
+ if not isinstance(s, StringNode):
+ raise ParseException('Key must be a string.',
+ self.getline(), s.lineno, s.colno)
+ if s.value in a.kwargs:
+ # + 1 to colno to point to the actual string, not the opening quote
+ raise ParseException('Duplicate dictionary key: {}'.format(s.value),
+ self.getline(), s.lineno, s.colno + 1)
+ a.set_kwarg(s.value, self.statement())
+ potential = self.current
+ if not self.accept('comma'):
+ return a
+ a.commas.append(potential)
+ else:
+ raise ParseException('Only key:value pairs are valid in dict construction.',
+ self.getline(), s.lineno, s.colno)
+ s = self.statement()
+ return a
+
def args(self):
s = self.statement()
a = ArgumentNode(s)
@@ -629,7 +672,7 @@ class Parser:
a.append(s)
elif self.accept('colon'):
if not isinstance(s, IdNode):
- raise ParseException('Keyword argument must be a plain identifier.',
+ raise ParseException('Dictionary key must be a plain identifier.',
self.getline(), s.lineno, s.colno)
a.set_kwarg(s.value, self.statement())
potential = self.current
@@ -664,10 +707,17 @@ class Parser:
t = self.current
self.expect('id')
varname = t
+ varnames = [t]
+
+ if self.accept('comma'):
+ t = self.current
+ self.expect('id')
+ varnames.append(t)
+
self.expect('colon')
items = self.statement()
block = self.codeblock()
- return ForeachClauseNode(varname.lineno, varname.colno, varname, items, block)
+ return ForeachClauseNode(varname.lineno, varname.colno, varnames, items, block)
def ifblock(self):
condition = self.statement()
diff --git a/run_unittests.py b/run_unittests.py
index 28d96a3..fb9fb03 100755
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -2328,6 +2328,20 @@ class FailureTests(BasePlatformTests):
self.assertEqual(cm.exception.returncode, 2)
self.wipe()
+ def test_dict_requires_key_value_pairs(self):
+ self.assertMesonRaises("dict = {3, 'foo': 'bar'}",
+ 'Only key:value pairs are valid in dict construction.')
+ self.assertMesonRaises("{'foo': 'bar', 3}",
+ 'Only key:value pairs are valid in dict construction.')
+
+ def test_dict_forbids_duplicate_keys(self):
+ self.assertMesonRaises("dict = {'a': 41, 'a': 42}",
+ 'Duplicate dictionary key: a.*')
+
+ def test_dict_forbids_integer_key(self):
+ self.assertMesonRaises("dict = {3: 'foo'}",
+ 'Key must be a string.*')
+
class WindowsTests(BasePlatformTests):
'''
diff --git a/test cases/common/199 dict/meson.build b/test cases/common/199 dict/meson.build
new file mode 100644
index 0000000..e1ee2e3
--- /dev/null
+++ b/test cases/common/199 dict/meson.build
@@ -0,0 +1,23 @@
+project('dict test', 'c')
+
+dict = {'foo' : 'bar',
+ 'baz' : 'foo',
+ 'foo bar': 'baz'}
+
+exe = executable('prog', sources : ['prog.c'])
+
+i = 0
+
+foreach key, value : dict
+ test('dict test @0@'.format(key), exe,
+ args : [dict[key], value])
+ i += 1
+endforeach
+
+assert(i == 3, 'There should be three elements in that dictionary')
+
+empty_dict = {}
+
+foreach key, value : empty_dict
+ assert(false, 'This dict should be empty')
+endforeach
diff --git a/test cases/common/199 dict/prog.c b/test cases/common/199 dict/prog.c
new file mode 100644
index 0000000..bf0999d
--- /dev/null
+++ b/test cases/common/199 dict/prog.c
@@ -0,0 +1,8 @@
+#include <string.h>
+
+int main(int argc, char **argv) {
+ if (argc != 3)
+ return 1;
+
+ return strcmp(argv[1], argv[2]);
+}
diff --git a/test cases/failing/75 run_command unclean exit/meson.build b/test cases/failing/75 run_command unclean exit/meson.build
new file mode 100644
index 0000000..4bc02ae
--- /dev/null
+++ b/test cases/failing/75 run_command unclean exit/meson.build
@@ -0,0 +1,4 @@
+project('run_command unclean exit', 'c')
+
+rcprog = find_program('./returncode.py')
+run_command(rcprog, '1', check : true)
diff --git a/test cases/failing/75 run_command unclean exit/returncode.py b/test cases/failing/75 run_command unclean exit/returncode.py
new file mode 100755
index 0000000..84dbc5d
--- /dev/null
+++ b/test cases/failing/75 run_command unclean exit/returncode.py
@@ -0,0 +1,4 @@
+#!/usr/bin/env python3
+
+import sys
+exit(int(sys.argv[1]))