Repository: mesonbuild/meson

Issue:
Support for samurai (alternate ninja implementation)
I'm the author of [samurai](https://github.com/michaelforney/samurai), an alternative implementation of ninja, and I'd like to see what it would take to get meson to support it.

It looks like the relevant code is here: https://github.com/mesonbuild/meson/blob/cb597adb0170a4818a4950ca9a4fe3f6d5a6c9d2/mesonbuild/environment.py#L106-L119

samurai doesn't support the `--version` option, and I am hesitant to add it for several reasons. However, it seems to me that meson shouldn't need to be doing this probing at all. The version is already checked by ninja itself using `ninja_required_version` in `build.ninja`, and if the user does not have it installed, they simply won't be able to start the build.

As far as I can tell, meson needs to know the ninja command for three purposes:
1. To support `ninja clean` as an alternative to `ninja -t clean`.
2. To support `ninja meson-scan-build` as an alternative to `scan-build ninja`.
3. To generate a compilation database during `build.ninja` generation.

Additionally, the version in `detect_ninja` is only used in one place in `run_tests.py`, presumably to make sure it supports `-w dupbuild=err`. However, earlier ninja versions will just fail because of an unsupported option.

One way to support samurai as well as any other future implementations would be to make a failure in `detect_ninja` just a warning, and in this case just omit a `clean` target. A failure in `ninja -t compdb` is already just a warning, so that could just be skipped if meson can't figure out the ninja command. Would this be acceptable?


Discussion:
Samurai looks good because it is built without any Python or C++ dependency. Obviously Meson requires Python, but Samurai seems a good step for anyone wanting to build a system without those dependencies.

I'm not a project maintainer, but as your question has been here a week, I'd thought I'd throw in my two cents if it offers any insight.

Meson likes to do a sanity check before the build. So letting the build fail when it comes to invoking Ninja for the build itself is probably not a good idea. I imagine this is based on the usability of Meson. A build is set up with `meson setup my_build_dir` and that checks everything is present and correct before the build is run. At some point there will be a `meson build my_build_dir` command that would be an abstraction including `ninja -C my_build_dir`. Discussions are on going about the implementation. Meson abstracts away the details of compilers, backends, etc. to make a very easy to use build system. I'm guessing ninja isn't invoked in the setup stage apart from just checking the version.

I suggest looking at MSVC more closely. In the mesonbuild/backend/ directory vs2010backend.py has the `autodetect_vs_version(build)` function. Then vs2015backend.py and vs2017backend.py have a very limited number of overriding methods. I'm wondering if Samurai should take the same approach and inherit from ninjabackend.py. The version detection in environment.py can be refactored out to ninjabackend.py.

Also note that `meson --help` shows `--backend {ninja,vs,vs2010,vs2015,vs2017,xcode}`. So some consideration needs to go in to how Samurai fits in there.

I hope that helps a little.

An alternative to Ninja would definitely be interesting, but I think if you want to replace Ninja, you may want to go the same route as clang and [pkgconf](http://pkgconf.org/) by providing a wrapper that accepts the same command-line arguments as Ninja.

Then the roadmap is to reach a reasonable amount of feature parity and then start to fix some of the issues with Ninja. Such as:

1. Targets cannot have [multiple outputs in the dependency files
](https://github.com/ninja-build/ninja/issues/1184)
2. Does not [delete non-empty directories](https://github.com/ninja-build/ninja/issues/828) on platforms other than Windows
3. All command-lines are a single string, which leads to quoting hell
4. Requires `sh` on platforms other than Windows because of (3)
5. Support a 'non-human-readable' format for command-lines that have special characters in them (such as newlines or tabs, or anything else that is hard/impossible to escape). Current Meson has to use a python wrapper for this.
6. More that I can't remember right now.

To be fair, (3) is one valid solution to the problem that on Windows [`CreateProcess()`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms682425(v=vs.85).aspx) is the native API and that accepts a single string as the command-line to run. `execve` and friends are [emulated by splitting that string](https://msdn.microsoft.com/en-us/library/windows/desktop/17w5ykft(v=vs.85).aspx).

However, I've come to believe that it may be more efficient and better for quoting/escaping if an array-like syntax for arguments was also supported, which on Windows would be converted to a single string command-line by doing the reverse of [CommandLineToArgvW()](https://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx) before passing to `CreateProcess()`.
As an aside, not having MSVC dependency handling is a deal-breaker for many people that use Meson since reliable and consistent cross-platform behavior is one of the upsides of Meson.
Thanks for your replies.

@astavale
> Meson likes to do a sanity check before the build. So letting the build fail when it comes to invoking Ninja for the build itself is probably not a good idea.I imagine this is based on the usability of Meson. A build is set up with meson setup my_build_dir and that checks everything is present and correct before the build is run.

I see your point, but I think a warning like "No ninja command detected; build may not succeed." would be sufficient. A user with a particular implementation in mind could simply run the appropriate command.

> At some point there will be a meson build my_build_dir command that would be an abstraction including ninja -C my_build_dir. Discussions are on going about the implementation. Meson abstracts away the details of compilers, backends, etc. to make a very easy to use build system.

Hmm... I don't really see the benefit for this, but if this is the case then I think meson would need to support samurai explicitly rather than any implementation of the users choice. Another option might be to add a flag to meson to explicitly specify the ninja command, skipping detection.

It also looks like the versioned VS backends don't do any sort of detection. `meson --backend vs2017` completes successfully for me with no warnings, even though I'm on linux with no VS.

> I'm guessing ninja isn't invoked in the setup stage apart from just checking the version.

It is invoked with `ninja -t compdb` to generate `compile_commands.json`, but a failure here is just a warning.

> I suggest looking at MSVC more closely. In the mesonbuild/backend/ directory vs2010backend.py has the autodetect_vs_version(build) function. Then vs2015backend.py and vs2017backend.py have a very limited number of overriding methods. I'm wondering if Samurai should take the same approach and inherit from ninjabackend.py. The version detection in environment.py can be refactored out to ninjabackend.py.
> 
> Also note that meson --help shows --backend {ninja,vs,vs2010,vs2015,vs2017,xcode}. So some consideration needs to go in to how Samurai fits in there.

That's an interesting idea. I hadn't considered a separate backend for samurai that inherits from the ninja backend. This is an option, but in general I'm trying to avoid special treatment of samurai in higher-level build systems. Ideally meson would be flexible enough to allow the user to chose which implementation to use (even if meson doesn't know about it).

@nirbheek 
> An alternative to Ninja would definitely be interesting, but I think if you want to replace Ninja, you may want to go the same route as clang and pkgconf by providing a wrapper that accepts the same command-line arguments as Ninja.

samurai *does* support the same command-line options as ninja. It intends to be a drop-in replacement, so a wrapper should not be necessary. It just does not implement 100% of them:
* `-n` (dryrun) and `-l` (load average job scheduling) are not yet supported.
* `-w` is not supported; multiple build lines for one target is always an error, and cycles are never allowed.
* `-t clean` is currently the only supported subtool. I may add others, but definitely not all of them.
* `-d explain` is currently the only supported debug mode.
* `--version` is currently not supported. It is also not clear what should be printed here (samurai's version? compatible ninja version?). I don't really want to print a ninja version since while samurai implements all the features of the build language, it is missing some subtools and debug modes. So meson wouldn't be able to use the version to decide which ones are available.

> Then the roadmap is to reach a reasonable amount of feature parity and then start to fix some of the issues with Ninja. Such as:

I agree that some of these are issues, but fixing them would require breaking compatibility with ninja, which is not one of samurai's goals.

> 1. Targets cannot have multiple outputs in the dependency files

To fix this, the format of .ninja_deps would need to change.

> 2. Does not delete non-empty directories on platforms other than Windows

I presume you are referring to https://github.com/mesonbuild/meson/issues/1644? This is a bit of a tangent, but I don't think recursively deleting non-empty directories with the same name as a "virtual" target is the right thing to do here. There might be important content in those directories.

I think the real issue here is that ninja uses `phony` to mean virtual target with no command while in make, `.PHONY` targets often also have associated commands. This could be solved by adding a special rule variable `phony = 1`, which just means that it has no output (so should not be cleaned) and is always out of date.

> 3. All command-lines are a single string, which leads to quoting hell
> 4. Requires sh on platforms other than Windows because of (3)
> 5. Support a 'non-human-readable' format for command-lines that have special characters in them (such as newlines or tabs, or anything else that is hard/impossible to escape). Current Meson has to use a python wrapper for this.
> 6. More that I can't remember right now.

Fixing these would probably involve backwards-incompatible changes to the build language.

> As an aside, not having MSVC dependency handling is a deal-breaker for many people that use Meson since reliable and consistent cross-platform behavior is one of the upsides of Meson.

A deal-breaker for who? Software developers using meson for their projects shouldn't really need to concerned about the particular backend that will be used. samurai only supports POSIX operating systems (at least for now), so users won't even run into MSVC-style header dependencies.
> That's an interesting idea. I hadn't considered a separate backend for samurai that inherits from the ninja backend. This is an option, but in general I'm trying to avoid special treatment of samurai in higher-level build systems. Ideally meson would be flexible enough to allow the user to chose which implementation to use (even if meson doesn't know about it).

Technically samurai is a different backend than ninja unless you implement all the features of ninja. Using the same build file format and almost implementing all the features of ninja does not qualify as a ninja replacement.. So until you reach feature parity I would have it as a separate backend in meson.

But anyway it would be interesting if a new build system was created that fixes all issues with ninja, breaking the file format if needed.
I would agree that Samurai doesn't strictly appear to be a "drop-in replacement". I've also just found [Shake](https://shakebuild.com/ninja), which is a [Haskell implementation of Ninja](https://github.com/ninja-build/ninja/wiki/Ninja-implementations).

Generalising from this, there is a Ninja build format that currently has three implementations that parse and process the build format. Sounds similar to MSVC and project files?

As far as I can tell there is no check for the version of the ninja file format, but there is for the implementation version. `ninja --version` is the version for the original Ninja implementation. `shake --version` is for Shake and if Samurai implemented --version then that would be for Samurai.

As an update, `samurai` does support `--version` now so its possible to work around the version check issue and complete builds by symlinking `ninja` to `samu`. This is not ideal because the user may or may not have `ninja` installed.

Resolving patch:
diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py
--- a/mesonbuild/environment.py
+++ b/mesonbuild/environment.py
@@ -115,7 +115,7 @@ def find_coverage_tools():
     return gcovr_exe, gcovr_new_rootdir, lcov_exe, genhtml_exe
 
 def detect_ninja(version='1.5', log=False):
-    for n in ['ninja', 'ninja-build']:
+    for n in ['ninja', 'ninja-build', 'samu']:
         try:
             p, found = Popen_safe([n, '--version'])[0:2]
         except (FileNotFoundError, PermissionError):
diff --git a/run_unittests.py b/run_unittests.py
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -924,11 +924,11 @@ def setUp(self):
         # Misc stuff
         self.orig_env = os.environ.copy()
         if self.backend is Backend.ninja:
-            self.no_rebuild_stdout = 'ninja: no work to do.'
+            self.no_rebuild_stdout = ['ninja: no work to do.', 'samu: nothing to do']
         else:
             # VS doesn't have a stable output when no changes are done
             # XCode backend is untested with unit tests, help welcome!
-            self.no_rebuild_stdout = 'UNKNOWN BACKEND {!r}'.format(self.backend.name)
+            self.no_rebuild_stdout = ['UNKNOWN BACKEND {!r}'.format(self.backend.name)]
 
         self.builddirs = []
         self.new_builddir()
@@ -1079,8 +1079,11 @@ def utime(self, f):
     def get_compdb(self):
         if self.backend is not Backend.ninja:
             raise unittest.SkipTest('Compiler db not available with {} backend'.format(self.backend.name))
-        with open(os.path.join(self.builddir, 'compile_commands.json')) as ifile:
-            contents = json.load(ifile)
+        try:
+            with open(os.path.join(self.builddir, 'compile_commands.json')) as ifile:
+                contents = json.load(ifile)
+        except FileNotFoundError:
+            raise unittest.SkipTest('Compiler db not found')
         # If Ninja is using .rsp files, generate them, read their contents, and
         # replace it as the command for all compile commands in the parsed json.
         if len(contents) > 0 and contents[0]['command'].endswith('.rsp'):
@@ -1134,7 +1137,7 @@ def assertPathBasenameEqual(self, path, basename):
     def assertBuildIsNoop(self):
         ret = self.build()
         if self.backend is Backend.ninja:
-            self.assertEqual(ret.split('\n')[-2], self.no_rebuild_stdout)
+            self.assertIn(ret.split('\n')[-2], self.no_rebuild_stdout)
         elif self.backend is Backend.vs:
             # Ensure that some target said that no rebuild was done
             self.assertIn('CustomBuild:\n  All outputs are up-to-date.', ret)


Test patch:
diff --git a/run_meson_command_tests.py b/run_meson_command_tests.py
--- a/run_meson_command_tests.py
+++ b/run_meson_command_tests.py
@@ -63,15 +63,14 @@ def tearDown(self):
 
     def _run(self, command, workdir=None):
         '''
-        Run a command while printing the stdout and stderr to stdout,
-        and also return a copy of it
+        Run a command while printing the stdout, and also return a copy of it
         '''
         # If this call hangs CI will just abort. It is very hard to distinguish
         # between CI issue and test bug in that case. Set timeout and fail loud
         # instead.
         p = subprocess.run(command, stdout=subprocess.PIPE,
-                           stderr=subprocess.STDOUT, env=os.environ.copy(),
-                           universal_newlines=True, cwd=workdir, timeout=60 * 5)
+                           env=os.environ.copy(), universal_newlines=True,
+                           cwd=workdir, timeout=60 * 5)
         print(p.stdout)
         if p.returncode != 0:
             raise subprocess.CalledProcessError(p.returncode, command)