diff --git a/docs/markdown/Release-notes-for-0.41.0.md b/docs/markdown/Release-notes-for-0.41.0.md index 9c02cdce0..a2a64dee4 100644 --- a/docs/markdown/Release-notes-for-0.41.0.md +++ b/docs/markdown/Release-notes-for-0.41.0.md @@ -17,3 +17,10 @@ Native support for linking against LLVM using the `dependency` function. The `fallback` keyword in `vcs_tag` is now optional. If not given, its value defaults to the return value of `meson.project_version()`. + +## Better quoting of special characters in ninja command invocations + +The ninja backend now quotes special characters that may be interpreted by +ninja itself, providing better interoperability with custom commands. This +support may not be perfect; please report any issues found with special +characters to the issue tracker. diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index eaf4cac0a..dad752bad 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -38,7 +38,12 @@ else: rmfile_prefix = 'rm -f {} &&' def ninja_quote(text): - return text.replace(' ', '$ ').replace(':', '$:') + for char in ('$', ' ', ':'): + text = text.replace(char, '$' + char) + if '\n' in text: + raise MesonException('Ninja does not support newlines in rules. ' + 'Please report this error with a test case to the Meson bug tracker.') + return text class NinjaBuildElement: @@ -480,12 +485,16 @@ int dummy; # If the target requires capturing stdout, then use the serialized # executable wrapper to capture that output and save it to a file. # + # If the command line requires a newline, also use the wrapper, as + # ninja does not support them in its build rule syntax. + # # Windows doesn't have -rpath, so for EXEs that need DLLs built within # the project, we need to set PATH so the DLLs are found. We use # a serialized executable wrapper for that and check if the # CustomTarget command needs extra paths first. - if target.capture or ((mesonlib.is_windows() or mesonlib.is_cygwin()) and - self.determine_windows_extra_paths(target.command[0])): + if (target.capture or any('\n' in c for c in cmd) or + ((mesonlib.is_windows() or mesonlib.is_cygwin()) and + self.determine_windows_extra_paths(target.command[0]))): exe_data = self.serialize_executable(target.command[0], cmd[1:], # All targets are built from the build dir self.environment.get_build_dir(), @@ -1448,12 +1457,13 @@ int dummy; def generate_swift_compile_rules(self, compiler, outfile): rule = 'rule %s_COMPILER\n' % compiler.get_language() - full_exe = [sys.executable, - self.environment.get_build_command(), + full_exe = [ninja_quote(sys.executable), + ninja_quote(self.environment.get_build_command()), '--internal', 'dirchanger', - '$RUNDIR'] + compiler.get_exelist() - invoc = ' '.join([ninja_quote(i) for i in full_exe]) + '$RUNDIR'] + invoc = (' '.join(full_exe) + ' ' + + ' '.join(ninja_quote(i) for i in compiler.get_exelist())) command = ' command = %s $ARGS $in\n' % invoc description = ' description = Compiling Swift source $in.\n' outfile.write(rule) diff --git a/test cases/common/149 special characters/check_quoting.py b/test cases/common/149 special characters/check_quoting.py new file mode 100644 index 000000000..d6e50ea69 --- /dev/null +++ b/test cases/common/149 special characters/check_quoting.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +import sys + +expected = { + 'newline': '\n', + 'dollar': '$', + 'colon': ':', + 'space': ' ', + 'multi1': ' ::$$ ::$$', + 'multi2': ' ::$$\n\n \n\n::$$', +} + +output = None + +for arg in sys.argv[1:]: + try: + name, value = arg.split('=', 1) + except ValueError: + output = arg + continue + + if expected[name] != value: + raise RuntimeError('{!r} is {!r} but should be {!r}'.format(name, value, expected[name])) + +if output is not None: + with open(output, 'w') as f: + f.write('Success!') diff --git a/test cases/common/149 special characters/installed_files.txt b/test cases/common/149 special characters/installed_files.txt new file mode 100644 index 000000000..f67788181 --- /dev/null +++ b/test cases/common/149 special characters/installed_files.txt @@ -0,0 +1,2 @@ +usr/share/result +usr/share/result2 diff --git a/test cases/common/149 special characters/meson.build b/test cases/common/149 special characters/meson.build new file mode 100644 index 000000000..ecba65003 --- /dev/null +++ b/test cases/common/149 special characters/meson.build @@ -0,0 +1,37 @@ +project('ninja special characters' ,'c') + +python = import('python3').find_python() + +# Without newlines, this should appear directly in build.ninja. +gen = custom_target('gen', + command : [ + python, + files('check_quoting.py'), + 'dollar=$', + 'colon=:', + 'space= ', + '''multi1= ::$$ ::$$''', + '@OUTPUT@'], + output : 'result', + install : true, + install_dir : get_option('datadir')) + +# With newlines, this should go through the exe wrapper. +gen2 = custom_target('gen2', + command : [ + python, + files('check_quoting.py'), + '''newline= +''', + 'dollar=$', + 'colon=:', + 'space= ', + '''multi2= ::$$ + + + +::$$''', + '@OUTPUT@'], + output : 'result2', + install : true, + install_dir : get_option('datadir'))