Repository: mesonbuild/meson

Issue:
Instead of just printing a warning, Meson should abort and exit with an error code if an unknown build option is used
**Describe the bug**
Meson currently only prints an easy to miss, especially in CI builds, warning if an unknown build option has been passed via `-D<option-name>=<value>`. It may not be that easy to miss if `meson configure` is used, but it is certainly easy to miss if `meson` is used to create a `build/` directory.

**To Reproduce**
Invoke meson with an argument that configured an unknown build option. For example.

```
$ meson -Dfoo=bar build-foo/
The Meson build system
Version: 0.52.1
Source dir: /home/user/code/barproject
Build dir: /home/user/code/barproject/build-foo
Build type: native build
WARNING: Unknown options: "foo"
Project name: barproject
Project version: 0.1
C compiler for the host machine: ccache cc (gcc 9.3.0 "cc (Gentoo 9.3.0 p2) 9.3.0")
C linker for the host machine: GNU ld.bfd 2.33.1
C++ compiler for the host machine: ccache c++ (gcc 9.3.0 "c++ (Gentoo 9.3.0 p2) 9.3.0")
C++ linker for the host machine: GNU ld.bfd 2.33.1
Host machine cpu family: x86_64
Host machine cpu: x86_64
Run-time dependency threads found: YES 
Configuring barproject_config.h using configuration
Found pkg-config: /usr/bin/pkg-config (0.29.2)
Run-time dependency GTest found: YES 1.9.0
Dependency threads found: YES (cached)
Build targets in project: 9
Found ninja-1.9.0.git.kitware.dyndep-1.jobserver-1 at /usr/bin/ninja
```

**Expected behavior**
The meson command should abort with an error code.



Discussion:
I have been burned by this recently as well. I think it's helpful to note that the behavior of silently dropping unknown options also affects the `default_options` block in the meson.build `project()`.

In my case, I was miss-spelling the `b_sanitize` option, resulting in none of the expected sanitizers being enabled.
Same here: I've lost count how many times I've misspelled built-in options - `pkg_config_path` vs `pkg-config-path`, `buildtype` vs `build_type`, `b_staticpic` vs `b_fpic` and etc and etc...
Another note: it's worse for `b_*` options (where I've experienced this) as there is not even a warning  emitted. In other words: `meson . b -Db_sanitize_not_really=none` does not complain at all even though `b_sanitize_not_really` is bogus.

For options that don't start with `b_*` (ie: aren't considered builtin options), one can use `--fatal-meson-warnings`. Unfortunately, this may turn some warnings that are being ignored purposely into errors (In my case, I've needed to support some custom `-fsanitize` flags to handle native true vs false builds, which results in meson warning that I should use `b_sanitize`).
When I added that warning the plan was to make it fatal. you may be in interested in the `--fatal-meson-warnings` option, which makes that a hard error.
We don't have an established policy for turning new warnings into hard errors, but I'd say that if it has been there for a few full releases we can do it.
It's been there a long time, it was one of my first contributions (back around the end of the .3x or early .4x IIRC). I'll go look
Well let's go for it then

Resolving patch:
diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py
--- a/mesonbuild/coredata.py
+++ b/mesonbuild/coredata.py
@@ -758,7 +758,7 @@ def copy_build_options_from_regular_ones(self) -> None:
                 except KeyError:
                     continue
 
-    def set_options(self, options: T.Dict[OptionKey, T.Any], subproject: str = '', warn_unknown: bool = True) -> None:
+    def set_options(self, options: T.Dict[OptionKey, T.Any], subproject: str = '') -> None:
         if not self.is_cross_build():
             options = {k: v for k, v in options.items() if k.machine is not MachineChoice.BUILD}
         # Set prefix first because it's needed to sanitize other options
@@ -774,16 +774,15 @@ def set_options(self, options: T.Dict[OptionKey, T.Any], subproject: str = '', w
         for k, v in options.items():
             if k == pfk:
                 continue
-            elif k not in self.options:
-                unknown_options.append(k)
-            else:
+            elif k in self.options:
                 self.set_option(k, v)
-        if unknown_options and warn_unknown:
+            elif k.machine != MachineChoice.BUILD:
+                unknown_options.append(k)
+        if unknown_options:
             unknown_options_str = ', '.join(sorted(str(s) for s in unknown_options))
             sub = f'In subproject {subproject}: ' if subproject else ''
-            mlog.warning(f'{sub}Unknown options: "{unknown_options_str}"')
-            mlog.log('The value of new options can be set with:')
-            mlog.log(mlog.bold('meson setup <builddir> --reconfigure -Dnew_option=new_value ...'))
+            raise MesonException(f'{sub}Unknown options: "{unknown_options_str}"')
+
         if not self.is_cross_build():
             self.copy_build_options_from_regular_ones()
 
diff --git a/run_unittests.py b/run_unittests.py
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -3324,8 +3324,6 @@ def test_lto_threads(self):
         if cc.get_id() == 'clang':
             if is_windows():
                 raise unittest.SkipTest('LTO not (yet) supported by windows clang')
-            else:
-                extra_args.append('-D_cargs=-Werror=unused-command-line-argument')
 
         self.init(testdir, extra_args=['-Db_lto=true', '-Db_lto_threads=8'] + extra_args)
         self.build()
@@ -4333,9 +4331,11 @@ def test_command_line(self):
             self.assertEqual(obj.options[OptionKey('default_library')].value, 'shared')
         self.wipe()
 
-        # Should warn on unknown options
-        out = self.init(testdir, extra_args=['-Dbad=1', '-Dfoo=2', '-Dwrong_link_args=foo'])
-        self.assertIn('Unknown options: "bad, foo, wrong_link_args"', out)
+        # Should fail on unknown options
+        with self.assertRaises((subprocess.CalledProcessError, RuntimeError)) as cm:
+            self.init(testdir, extra_args=['-Dbad=1', '-Dfoo=2', '-Dwrong_link_args=foo'])
+            self.assertNotEqual(0, cm.exception.returncode)
+            self.assertIn(msg, cm.exception.output)
         self.wipe()
 
         # Should fail on malformed option


Test patch: