Repository: mesonbuild/meson

Issue:
[Feature request] Add `meson compile` command
Proposal:
Backend-agnostic build command `meson compile` - counterpart of `cmake --build`.

Reasoning:
- Simplifies `meson` usage and integration: the same `meson` commands could be invoked for all backends.
- Reduces the amount of knowledge required to use `meson` out-of-the-box (i.e. with default `ninja` backend). Since user won't have to know anything about ninja (apart from the fact that it's needed).

Proposed interface (based on `cmake --build):
```
usage: meson compile [-h] [-C WD] [--clean] [--backend-args ARGS] [args [args ...]]

positional arguments:
  args                            optional list of targets to build

optional arguments:
-h, --help                      show this help message and exit
-C WD                          directory to cd into before running
--clean                          invokes clean command of the native tool.
--backend-args ARGS    arguments to pass to the native tool.
```


Discussion:
Note: most of the implementation code might be directly lifted from the test runner: https://github.com/mesonbuild/meson/blob/d8faf9b7062e2278adddb3b107ea58784fe544d4/run_tests.py#L211
Makes sense to me, we added 'meson install' recently too. Are you working on a patch?
Replaced `--target` with positional `[args]` for consistency with `meson test` and `meson install`.
>  Are you working on a patch?

Not yet, since my free time is currently pretty lacking. I will, however, take this on if nobody starts working on it in the next month or so (that is, a month from now, not the next calendar month :P).
As the person developing the meson.build file itself, I would also like to have a single command `meson setup_and_compile --wipe_ignore_missing`. Would save writing wrapper scripts on top of meson (on top of ninja). See https://github.com/mesonbuild/meson/issues/6434#issuecomment-572290358

And a poney.

Resolving patch:
diff --git a/mesonbuild/mcompile.py b/mesonbuild/mcompile.py
new file mode 100644
--- /dev/null
+++ b/mesonbuild/mcompile.py
@@ -0,0 +1,123 @@
+# Copyright 2020 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Entrypoint script for backend agnostic compile."""
+
+import os
+import pathlib
+import shutil
+import sys
+import typing as T
+
+from . import mlog
+from . import mesonlib
+from .mesonlib import MesonException
+
+if T.TYPE_CHECKING:
+    import argparse
+
+
+def add_arguments(parser: 'argparse.ArgumentParser') -> None:
+    """Add compile specific arguments."""
+    parser.add_argument(
+        '-j', '--jobs',
+        action='store',
+        default=0,
+        type=int,
+        help='The number of worker jobs to run (if supported). If the value is less than 1 the build program will guess.'
+    )
+    parser.add_argument(
+        '-l', '--load-average',
+        action='store',
+        default=0,
+        type=int,
+        help='The system load average to try to maintain (if supported)'
+    )
+    parser.add_argument(
+        '--clean',
+        action='store_true',
+        help='Clean the build directory.'
+    )
+    parser.add_argument(
+        '-C',
+        action='store',
+        dest='builddir',
+        type=pathlib.Path,
+        required=True,
+        default='.',
+        help='The directory containing build files to be built.'
+    )
+
+
+def run(options: 'argparse.Namespace') -> int:
+    bdir = options.builddir  # type: pathlib.Path
+    if not bdir.exists():
+        raise MesonException('Path to builddir {} does not exist!'.format(str(bdir.resolve())))
+    if not bdir.is_dir():
+        raise MesonException('builddir path should be a directory.')
+
+    cmd = []  # type: T.List[str]
+    runner = None  # type T.Optional[str]
+    slns = list(bdir.glob('*.sln'))
+
+    if (bdir / 'build.ninja').exists():
+        runner = os.environ.get('NINJA')
+        if not runner:
+            if shutil.which('ninja'):
+                runner = 'ninja'
+            elif shutil.which('samu'):
+                runner = 'samu'
+
+        if runner is None:
+            raise MesonException('Cannot find either ninja or samu.')
+
+        cmd = [runner, '-C', bdir.as_posix()]
+
+        # If the value is set to < 1 then don't set anything, which let's
+        # ninja/samu decide what to do.
+        if options.jobs > 0:
+            cmd.extend(['-j', str(options.jobs)])
+        if options.load_average > 0:
+            cmd.extend(['-l', str(options.load_average)])
+        if options.clean:
+            cmd.append('clean')
+
+    # TODO: with python 3.8 this could be `elif slns := bdir.glob('*.sln'):`
+    elif slns:
+        assert len(slns) == 1, 'More than one solution in a project?'
+
+        sln = slns[0]
+        cmd = ['msbuild', str(sln.resolve())]
+
+        # In msbuild `-m` with no number means "detect cpus", the default is `-m1`
+        if options.jobs > 0:
+            cmd.append('-m{}'.format(options.jobs))
+        else:
+            cmd.append('-m')
+
+        if options.load_average:
+            mlog.warning('Msbuild does not have a load-average switch, ignoring.')
+        if options.clean:
+            cmd.extend(['/t:Clean'])
+
+    # TODO: xcode?
+    else:
+        raise MesonException(
+            'Could not find any runner or backend for directory {}'.format(bdir.resolve().as_posix()))
+
+    mlog.log('Found runner:', runner)
+
+    p, *_ = mesonlib.Popen_safe(cmd, stdout=sys.stdout.buffer, stderr=sys.stderr.buffer)
+
+    return p.returncode
diff --git a/mesonbuild/mesonmain.py b/mesonbuild/mesonmain.py
--- a/mesonbuild/mesonmain.py
+++ b/mesonbuild/mesonmain.py
@@ -22,7 +22,7 @@
 
 from . import mesonlib
 from . import mlog
-from . import mconf, mdist, minit, minstall, mintro, msetup, mtest, rewriter, msubprojects, munstable_coredata
+from . import mconf, mdist, minit, minstall, mintro, msetup, mtest, rewriter, msubprojects, munstable_coredata, mcompile
 from .mesonlib import MesonException
 from .environment import detect_msys2_arch
 from .wrap import wraptool
@@ -62,6 +62,8 @@ def __init__(self):
                          help_msg='Print help of a subcommand')
         self.add_command('rewrite', lambda parser: rewriter.add_arguments(parser, self.formatter), rewriter.run,
                          help_msg='Modify the project definition')
+        self.add_command('compile', mcompile.add_arguments, mcompile.run,
+                         help_msg='Build the project')
 
         # Hidden commands
         self.add_command('runpython', self.add_runpython_arguments, self.run_runpython_command,
diff --git a/run_unittests.py b/run_unittests.py
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -4412,6 +4412,21 @@ def test_summary(self):
         else:
             self.assertEqual(expected_lines, out_lines)
 
+    def test_meson_compile(self):
+        """Test the meson compile command."""
+        prog = 'trivialprog'
+        if is_windows():
+            prog = '{}.exe'.format(prog)
+
+        testdir = os.path.join(self.common_test_dir, '1 trivial')
+        self.init(testdir)
+        self._run([*self.meson_command, 'compile', '-C', self.builddir])
+        # If compile worked then we should get a program
+        self.assertPathExists(os.path.join(self.builddir, prog))
+
+        self._run([*self.meson_command, 'compile', '-C', self.builddir, '--clean'])
+        self.assertPathDoesNotExist(os.path.join(self.builddir, prog))
+
 
 class FailureTests(BasePlatformTests):
     '''


Test patch: