Refactor option classes to their own file.
diff --git a/mesonbuild/ast/introspection.py b/mesonbuild/ast/introspection.py
index fa11feb..c7dcf73 100644
--- a/mesonbuild/ast/introspection.py
+++ b/mesonbuild/ast/introspection.py
@@ -10,7 +10,7 @@
 import os
 import typing as T
 
-from .. import compilers, environment, mesonlib, optinterpreter
+from .. import compilers, environment, mesonlib, optinterpreter, options
 from .. import coredata as cdata
 from ..build import Executable, Jar, SharedLibrary, SharedModule, StaticLibrary
 from ..compilers import detect_compiler_for
@@ -150,8 +150,8 @@
     def func_add_languages(self, node: BaseNode, args: T.List[TYPE_var], kwargs: T.Dict[str, TYPE_var]) -> None:
         kwargs = self.flatten_kwargs(kwargs)
         required = kwargs.get('required', True)
-        assert isinstance(required, (bool, cdata.UserFeatureOption)), 'for mypy'
-        if isinstance(required, cdata.UserFeatureOption):
+        assert isinstance(required, (bool, options.UserFeatureOption)), 'for mypy'
+        if isinstance(required, options.UserFeatureOption):
             required = required.is_enabled()
         if 'native' in kwargs:
             native = kwargs.get('native', False)
diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py
index e1b0928..57f2bed 100644
--- a/mesonbuild/cargo/interpreter.py
+++ b/mesonbuild/cargo/interpreter.py
@@ -23,7 +23,7 @@
 from . import builder
 from . import version
 from ..mesonlib import MesonException, Popen_safe, OptionKey
-from .. import coredata
+from .. import coredata, options
 
 if T.TYPE_CHECKING:
     from types import ModuleType
@@ -712,11 +712,11 @@
     build = builder.Builder(filename)
 
     # Generate project options
-    options: T.Dict[OptionKey, coredata.UserOption] = {}
+    project_options: T.Dict[OptionKey, options.UserOption] = {}
     for feature in cargo.features:
         key = OptionKey(_option_name(feature), subproject=subp_name)
         enabled = feature == 'default'
-        options[key] = coredata.UserBooleanOption(key.name, f'Cargo {feature} feature', enabled)
+        project_options[key] = options.UserBooleanOption(key.name, f'Cargo {feature} feature', enabled)
 
     ast = _create_project(cargo, build)
     ast += [build.assign(build.function('import', [build.string('rust')]), 'rust')]
@@ -730,4 +730,4 @@
         for crate_type in cargo.lib.crate_type:
             ast.extend(_create_lib(cargo, build, crate_type))
 
-    return build.block(ast), options
+    return build.block(ast), project_options
diff --git a/mesonbuild/compilers/c.py b/mesonbuild/compilers/c.py
index 18b25d4..d7c139e 100644
--- a/mesonbuild/compilers/c.py
+++ b/mesonbuild/compilers/c.py
@@ -7,6 +7,7 @@
 import typing as T
 
 from .. import coredata
+from .. import options
 from .. import mlog
 from ..mesonlib import MesonException, version_compare, OptionKey
 from .c_function_attributes import C_FUNC_ATTRIBUTES
@@ -96,7 +97,7 @@
         opts = super().get_options()
         key = OptionKey('std', machine=self.for_machine, lang=self.language)
         opts.update({
-            key: coredata.UserStdOption('C', _ALL_STDS),
+            key: options.UserStdOption('C', _ALL_STDS),
         })
         return opts
 
@@ -128,7 +129,7 @@
         if version_compare(self.version, self._C23_VERSION):
             stds += ['c23']
         std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(stds, gnu=True)
         return opts
 
@@ -154,7 +155,7 @@
         if self.info.is_windows() or self.info.is_cygwin():
             self.update_options(
                 opts,
-                self.create_option(coredata.UserArrayOption,
+                self.create_option(options.UserArrayOption,
                                    OptionKey('winlibs', machine=self.for_machine, lang=self.language),
                                    'Standard Win libraries to link against',
                                    gnu_winlibs),
@@ -247,7 +248,7 @@
     def get_options(self) -> 'MutableKeyedOptionDictType':
         opts = CCompiler.get_options(self)
         std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(['c90', 'c99', 'c11'], gnu=True)
         return opts
 
@@ -298,12 +299,12 @@
             stds += ['c23']
         key = OptionKey('std', machine=self.for_machine, lang=self.language)
         std_opt = opts[key]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(stds, gnu=True)
         if self.info.is_windows() or self.info.is_cygwin():
             self.update_options(
                 opts,
-                self.create_option(coredata.UserArrayOption,
+                self.create_option(options.UserArrayOption,
                                    key.evolve('winlibs'),
                                    'Standard Win libraries to link against',
                                    gnu_winlibs),
@@ -377,7 +378,7 @@
         if version_compare(self.version, '>=1.26.00'):
             stds += ['c17', 'c18', 'iso9899:2017', 'iso9899:2018', 'gnu17', 'gnu18']
         std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(stds)
         return opts
 
@@ -416,7 +417,7 @@
         if version_compare(self.version, '>=16.0.0'):
             stds += ['c11']
         std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(stds, gnu=True)
         return opts
 
@@ -441,7 +442,7 @@
         return self.update_options(
             super().get_options(),
             self.create_option(
-                coredata.UserArrayOption,
+                options.UserArrayOption,
                 OptionKey('winlibs', machine=self.for_machine, lang=self.language),
                 'Windows libs to link against.',
                 msvc_winlibs,
@@ -480,7 +481,7 @@
         if version_compare(self.version, self._C17_VERSION):
             stds += ['c17', 'c18']
         std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(stds, gnu=True, gnu_deprecated=True)
         return opts
 
@@ -529,7 +530,7 @@
     def get_options(self) -> 'MutableKeyedOptionDictType':
         opts = super().get_options()
         std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(['c89', 'c99', 'c11'])
         return opts
 
@@ -562,7 +563,7 @@
     def get_options(self) -> 'MutableKeyedOptionDictType':
         opts = CCompiler.get_options(self)
         std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(['c89', 'c99', 'c11'])
         return opts
 
@@ -591,7 +592,7 @@
     def get_options(self) -> 'MutableKeyedOptionDictType':
         opts = CCompiler.get_options(self)
         std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(['c89', 'c99'])
         return opts
 
@@ -638,7 +639,7 @@
     def get_options(self) -> 'MutableKeyedOptionDictType':
         opts = CCompiler.get_options(self)
         std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(['c89', 'c99'], gnu=True)
         return opts
 
@@ -683,7 +684,7 @@
     def get_options(self) -> 'MutableKeyedOptionDictType':
         opts = CCompiler.get_options(self)
         std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(['c89', 'c99'])
         return opts
 
@@ -720,7 +721,7 @@
     def get_options(self) -> 'MutableKeyedOptionDictType':
         opts = CCompiler.get_options(self)
         std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(['c89', 'c99', 'c11'])
         return opts
 
diff --git a/mesonbuild/compilers/compilers.py b/mesonbuild/compilers/compilers.py
index 44b998a..87444b4 100644
--- a/mesonbuild/compilers/compilers.py
+++ b/mesonbuild/compilers/compilers.py
@@ -15,6 +15,7 @@
 from .. import coredata
 from .. import mlog
 from .. import mesonlib
+from .. import options
 from ..mesonlib import (
     HoldableObject,
     EnvironmentException, MesonException,
@@ -35,7 +36,7 @@
 
     CompilerType = T.TypeVar('CompilerType', bound='Compiler')
     _T = T.TypeVar('_T')
-    UserOptionType = T.TypeVar('UserOptionType', bound=coredata.UserOption)
+    UserOptionType = T.TypeVar('UserOptionType', bound=options.UserOption)
 
 """This file contains the data files of all compilers Meson knows
 about. To support a new compiler, add its information below.
@@ -209,40 +210,40 @@
 MSCRT_VALS = ['none', 'md', 'mdd', 'mt', 'mtd']
 
 @dataclass
-class BaseOption(T.Generic[coredata._T, coredata._U]):
-    opt_type: T.Type[coredata._U]
+class BaseOption(T.Generic[options._T, options._U]):
+    opt_type: T.Type[options._U]
     description: str
     default: T.Any = None
     choices: T.Any = None
 
-    def init_option(self, name: OptionKey) -> coredata._U:
+    def init_option(self, name: OptionKey) -> options._U:
         keywords = {'value': self.default}
         if self.choices:
             keywords['choices'] = self.choices
         return self.opt_type(name.name, self.description, **keywords)
 
 BASE_OPTIONS: T.Mapping[OptionKey, BaseOption] = {
-    OptionKey('b_pch'): BaseOption(coredata.UserBooleanOption, 'Use precompiled headers', True),
-    OptionKey('b_lto'): BaseOption(coredata.UserBooleanOption, 'Use link time optimization', False),
-    OptionKey('b_lto_threads'): BaseOption(coredata.UserIntegerOption, 'Use multiple threads for Link Time Optimization', (None, None, 0)),
-    OptionKey('b_lto_mode'): BaseOption(coredata.UserComboOption, 'Select between different LTO modes.', 'default',
+    OptionKey('b_pch'): BaseOption(options.UserBooleanOption, 'Use precompiled headers', True),
+    OptionKey('b_lto'): BaseOption(options.UserBooleanOption, 'Use link time optimization', False),
+    OptionKey('b_lto_threads'): BaseOption(options.UserIntegerOption, 'Use multiple threads for Link Time Optimization', (None, None, 0)),
+    OptionKey('b_lto_mode'): BaseOption(options.UserComboOption, 'Select between different LTO modes.', 'default',
                                         choices=['default', 'thin']),
-    OptionKey('b_thinlto_cache'): BaseOption(coredata.UserBooleanOption, 'Use LLVM ThinLTO caching for faster incremental builds', False),
-    OptionKey('b_thinlto_cache_dir'): BaseOption(coredata.UserStringOption, 'Directory to store ThinLTO cache objects', ''),
-    OptionKey('b_sanitize'): BaseOption(coredata.UserComboOption, 'Code sanitizer to use', 'none',
+    OptionKey('b_thinlto_cache'): BaseOption(options.UserBooleanOption, 'Use LLVM ThinLTO caching for faster incremental builds', False),
+    OptionKey('b_thinlto_cache_dir'): BaseOption(options.UserStringOption, 'Directory to store ThinLTO cache objects', ''),
+    OptionKey('b_sanitize'): BaseOption(options.UserComboOption, 'Code sanitizer to use', 'none',
                                         choices=['none', 'address', 'thread', 'undefined', 'memory', 'leak', 'address,undefined']),
-    OptionKey('b_lundef'): BaseOption(coredata.UserBooleanOption, 'Use -Wl,--no-undefined when linking', True),
-    OptionKey('b_asneeded'): BaseOption(coredata.UserBooleanOption, 'Use -Wl,--as-needed when linking', True),
-    OptionKey('b_pgo'): BaseOption(coredata.UserComboOption, 'Use profile guided optimization', 'off',
+    OptionKey('b_lundef'): BaseOption(options.UserBooleanOption, 'Use -Wl,--no-undefined when linking', True),
+    OptionKey('b_asneeded'): BaseOption(options.UserBooleanOption, 'Use -Wl,--as-needed when linking', True),
+    OptionKey('b_pgo'): BaseOption(options.UserComboOption, 'Use profile guided optimization', 'off',
                                    choices=['off', 'generate', 'use']),
-    OptionKey('b_coverage'): BaseOption(coredata.UserBooleanOption, 'Enable coverage tracking.', False),
-    OptionKey('b_colorout'): BaseOption(coredata.UserComboOption, 'Use colored output', 'always',
+    OptionKey('b_coverage'): BaseOption(options.UserBooleanOption, 'Enable coverage tracking.', False),
+    OptionKey('b_colorout'): BaseOption(options.UserComboOption, 'Use colored output', 'always',
                                         choices=['auto', 'always', 'never']),
-    OptionKey('b_ndebug'): BaseOption(coredata.UserComboOption, 'Disable asserts', 'false', choices=['true', 'false', 'if-release']),
-    OptionKey('b_staticpic'): BaseOption(coredata.UserBooleanOption, 'Build static libraries as position independent', True),
-    OptionKey('b_pie'): BaseOption(coredata.UserBooleanOption, 'Build executables as position independent', False),
-    OptionKey('b_bitcode'): BaseOption(coredata.UserBooleanOption, 'Generate and embed bitcode (only macOS/iOS/tvOS)', False),
-    OptionKey('b_vscrt'): BaseOption(coredata.UserComboOption, 'VS run-time library type to use.', 'from_buildtype',
+    OptionKey('b_ndebug'): BaseOption(options.UserComboOption, 'Disable asserts', 'false', choices=['true', 'false', 'if-release']),
+    OptionKey('b_staticpic'): BaseOption(options.UserBooleanOption, 'Build static libraries as position independent', True),
+    OptionKey('b_pie'): BaseOption(options.UserBooleanOption, 'Build executables as position independent', False),
+    OptionKey('b_bitcode'): BaseOption(options.UserBooleanOption, 'Generate and embed bitcode (only macOS/iOS/tvOS)', False),
+    OptionKey('b_vscrt'): BaseOption(options.UserComboOption, 'VS run-time library type to use.', 'from_buildtype',
                                      choices=MSCRT_VALS + ['from_buildtype', 'static_from_buildtype']),
 }
 
@@ -1365,12 +1366,12 @@
     comp_options = env.options.get(comp_key, [])
     link_options = env.options.get(largkey, [])
 
-    cargs = coredata.UserArrayOption(
+    cargs = options.UserArrayOption(
         f'{lang}_{argkey.name}',
         description + ' compiler',
         comp_options, split_args=True, allow_dups=True)
 
-    largs = coredata.UserArrayOption(
+    largs = options.UserArrayOption(
         f'{lang}_{largkey.name}',
         description + ' linker',
         link_options, split_args=True, allow_dups=True)
diff --git a/mesonbuild/compilers/cpp.py b/mesonbuild/compilers/cpp.py
index 525c9fc..505bd56 100644
--- a/mesonbuild/compilers/cpp.py
+++ b/mesonbuild/compilers/cpp.py
@@ -9,6 +9,7 @@
 import typing as T
 
 from .. import coredata
+from .. import options
 from .. import mlog
 from ..mesonlib import MesonException, version_compare, OptionKey
 
@@ -174,7 +175,7 @@
         opts = super().get_options()
         key = OptionKey('std', machine=self.for_machine, lang=self.language)
         opts.update({
-            key: coredata.UserStdOption('C++', _ALL_STDS),
+            key: options.UserStdOption('C++', _ALL_STDS),
         })
         return opts
 
@@ -242,16 +243,16 @@
         key = OptionKey('key', machine=self.for_machine, lang=self.language)
         self.update_options(
             opts,
-            self.create_option(coredata.UserComboOption,
+            self.create_option(options.UserComboOption,
                                key.evolve('eh'),
                                'C++ exception handling type.',
                                ['none', 'default', 'a', 's', 'sc'],
                                'default'),
-            self.create_option(coredata.UserBooleanOption,
+            self.create_option(options.UserBooleanOption,
                                key.evolve('rtti'),
                                'Enable RTTI',
                                True),
-            self.create_option(coredata.UserBooleanOption,
+            self.create_option(options.UserBooleanOption,
                                key.evolve('debugstl'),
                                'STL debug mode',
                                False),
@@ -264,12 +265,12 @@
         if version_compare(self.version, self._CPP26_VERSION):
             cppstd_choices.append('c++26')
         std_opt = opts[key.evolve('std')]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(cppstd_choices, gnu=True)
         if self.info.is_windows() or self.info.is_cygwin():
             self.update_options(
                 opts,
-                self.create_option(coredata.UserArrayOption,
+                self.create_option(options.UserArrayOption,
                                    key.evolve('winlibs'),
                                    'Standard Win libraries to link against',
                                    gnu_winlibs),
@@ -393,14 +394,14 @@
         key = OptionKey('std', machine=self.for_machine, lang=self.language)
         self.update_options(
             opts,
-            self.create_option(coredata.UserComboOption,
+            self.create_option(options.UserComboOption,
                                key.evolve('eh'),
                                'C++ exception handling type.',
                                ['none', 'default', 'a', 's', 'sc'],
                                'default'),
         )
         std_opt = opts[key]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(['c++98', 'c++03', 'c++11', 'c++14', 'c++17'], gnu=True)
         return opts
 
@@ -442,16 +443,16 @@
         opts = CPPCompiler.get_options(self)
         self.update_options(
             opts,
-            self.create_option(coredata.UserComboOption,
+            self.create_option(options.UserComboOption,
                                key.evolve('eh'),
                                'C++ exception handling type.',
                                ['none', 'default', 'a', 's', 'sc'],
                                'default'),
-            self.create_option(coredata.UserBooleanOption,
+            self.create_option(options.UserBooleanOption,
                                key.evolve('rtti'),
                                'Enable RTTI',
                                True),
-            self.create_option(coredata.UserBooleanOption,
+            self.create_option(options.UserBooleanOption,
                                key.evolve('debugstl'),
                                'STL debug mode',
                                False),
@@ -465,12 +466,12 @@
         if version_compare(self.version, '>=14.0.0'):
             cppstd_choices.append('c++26')
         std_opt = opts[key]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(cppstd_choices, gnu=True)
         if self.info.is_windows() or self.info.is_cygwin():
             self.update_options(
                 opts,
-                self.create_option(coredata.UserArrayOption,
+                self.create_option(options.UserArrayOption,
                                    key.evolve('winlibs'),
                                    'Standard Win libraries to link against',
                                    gnu_winlibs),
@@ -582,18 +583,18 @@
         key = OptionKey('std', machine=self.for_machine, lang=self.language)
         self.update_options(
             opts,
-            self.create_option(coredata.UserComboOption,
+            self.create_option(options.UserComboOption,
                                key.evolve('eh'),
                                'C++ exception handling type.',
                                ['none', 'default', 'a', 's', 'sc'],
                                'default'),
-            self.create_option(coredata.UserBooleanOption,
+            self.create_option(options.UserBooleanOption,
                                key.evolve('debugstl'),
                                'STL debug mode',
                                False),
         )
         std_opt = opts[key]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(cpp_stds, gnu=True)
         return opts
 
@@ -661,22 +662,22 @@
         key = OptionKey('std', machine=self.for_machine, lang=self.language)
         self.update_options(
             opts,
-            self.create_option(coredata.UserComboOption,
+            self.create_option(options.UserComboOption,
                                key.evolve('eh'),
                                'C++ exception handling type.',
                                ['none', 'default', 'a', 's', 'sc'],
                                'default'),
-            self.create_option(coredata.UserBooleanOption,
+            self.create_option(options.UserBooleanOption,
                                key.evolve('rtti'),
                                'Enable RTTI',
                                True),
-            self.create_option(coredata.UserBooleanOption,
+            self.create_option(options.UserBooleanOption,
                                key.evolve('debugstl'),
                                'STL debug mode',
                                False),
         )
         std_opt = opts[key]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(c_stds + g_stds)
         return opts
 
@@ -734,22 +735,22 @@
         key = OptionKey('std', machine=self.for_machine, lang=self.language)
         self.update_options(
             opts,
-            self.create_option(coredata.UserComboOption,
+            self.create_option(options.UserComboOption,
                                key.evolve('eh'),
                                'C++ exception handling type.',
                                ['none', 'default', 'a', 's', 'sc'],
                                'default'),
-            self.create_option(coredata.UserBooleanOption,
+            self.create_option(options.UserBooleanOption,
                                key.evolve('rtti'),
                                'Enable RTTI',
                                True),
-            self.create_option(coredata.UserArrayOption,
+            self.create_option(options.UserArrayOption,
                                key.evolve('winlibs'),
                                'Windows libs to link against.',
                                msvc_winlibs),
         )
         std_opt = opts[key]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(cpp_stds)
         return opts
 
@@ -912,7 +913,7 @@
     def get_options(self) -> 'MutableKeyedOptionDictType':
         opts = CPPCompiler.get_options(self)
         std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(['c++03', 'c++11'])
         return opts
 
@@ -973,7 +974,7 @@
     def get_options(self) -> 'MutableKeyedOptionDictType':
         opts = CPPCompiler.get_options(self)
         std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)]
-        assert isinstance(std_opt, coredata.UserStdOption), 'for mypy'
+        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
         std_opt.set_versions(['c++03'])
         return opts
 
diff --git a/mesonbuild/compilers/cuda.py b/mesonbuild/compilers/cuda.py
index 3761019..7061624 100644
--- a/mesonbuild/compilers/cuda.py
+++ b/mesonbuild/compilers/cuda.py
@@ -9,6 +9,7 @@
 import typing as T
 
 from .. import coredata
+from .. import options
 from .. import mlog
 from ..mesonlib import (
     EnvironmentException, Popen_safe,
@@ -643,12 +644,12 @@
 
         return self.update_options(
             super().get_options(),
-            self.create_option(coredata.UserComboOption,
+            self.create_option(options.UserComboOption,
                                OptionKey('std', machine=self.for_machine, lang=self.language),
                                'C++ language standard to use with CUDA',
                                cpp_stds,
                                'none'),
-            self.create_option(coredata.UserStringOption,
+            self.create_option(options.UserStringOption,
                                OptionKey('ccbindir', machine=self.for_machine, lang=self.language),
                                'CUDA non-default toolchain directory to use (-ccbin)',
                                ''),
diff --git a/mesonbuild/compilers/cython.py b/mesonbuild/compilers/cython.py
index 30cec81..409a483 100644
--- a/mesonbuild/compilers/cython.py
+++ b/mesonbuild/compilers/cython.py
@@ -6,7 +6,7 @@
 
 import typing as T
 
-from .. import coredata
+from .. import coredata, options
 from ..mesonlib import EnvironmentException, OptionKey, version_compare
 from .compilers import Compiler
 
@@ -69,12 +69,12 @@
     def get_options(self) -> 'MutableKeyedOptionDictType':
         return self.update_options(
             super().get_options(),
-            self.create_option(coredata.UserComboOption,
+            self.create_option(options.UserComboOption,
                                OptionKey('version', machine=self.for_machine, lang=self.language),
                                'Python version to target',
                                ['2', '3'],
                                '3'),
-            self.create_option(coredata.UserComboOption,
+            self.create_option(options.UserComboOption,
                                OptionKey('language', machine=self.for_machine, lang=self.language),
                                'Output C or C++ files',
                                ['c', 'cpp'],
diff --git a/mesonbuild/compilers/fortran.py b/mesonbuild/compilers/fortran.py
index 4282515..af6e5d6 100644
--- a/mesonbuild/compilers/fortran.py
+++ b/mesonbuild/compilers/fortran.py
@@ -7,6 +7,7 @@
 import os
 
 from .. import coredata
+from .. import options
 from .compilers import (
     clike_debug_args,
     Compiler,
@@ -114,7 +115,7 @@
     def get_options(self) -> 'MutableKeyedOptionDictType':
         return self.update_options(
             super().get_options(),
-            self.create_option(coredata.UserComboOption,
+            self.create_option(options.UserComboOption,
                                OptionKey('std', machine=self.for_machine, lang=self.language),
                                'Fortran language standard to use',
                                ['none'],
diff --git a/mesonbuild/compilers/mixins/emscripten.py b/mesonbuild/compilers/mixins/emscripten.py
index bb8a520..110dbc6 100644
--- a/mesonbuild/compilers/mixins/emscripten.py
+++ b/mesonbuild/compilers/mixins/emscripten.py
@@ -9,6 +9,7 @@
 import typing as T
 
 from ... import coredata
+from ... import options
 from ... import mesonlib
 from ...mesonlib import OptionKey
 from ...mesonlib import LibType
@@ -59,7 +60,7 @@
         return self.update_options(
             super().get_options(),
             self.create_option(
-                coredata.UserIntegerOption,
+                options.UserIntegerOption,
                 OptionKey('thread_count', machine=self.for_machine, lang=self.language),
                 'Number of threads to use in web assembly, set to 0 to disable',
                 (0, None, 4),  # Default was picked at random
diff --git a/mesonbuild/compilers/objc.py b/mesonbuild/compilers/objc.py
index 7c19c1b..4d33ec8 100644
--- a/mesonbuild/compilers/objc.py
+++ b/mesonbuild/compilers/objc.py
@@ -6,6 +6,7 @@
 import typing as T
 
 from .. import coredata
+from .. import options
 from ..mesonlib import OptionKey
 
 from .compilers import Compiler
@@ -80,7 +81,7 @@
     def get_options(self) -> 'coredata.MutableKeyedOptionDictType':
         return self.update_options(
             super().get_options(),
-            self.create_option(coredata.UserComboOption,
+            self.create_option(options.UserComboOption,
                                OptionKey('std', machine=self.for_machine, lang='c'),
                                'C language standard to use',
                                ['none', 'c89', 'c99', 'c11', 'c17', 'gnu89', 'gnu99', 'gnu11', 'gnu17'],
diff --git a/mesonbuild/compilers/objcpp.py b/mesonbuild/compilers/objcpp.py
index 46eaa50..e28e3ed 100644
--- a/mesonbuild/compilers/objcpp.py
+++ b/mesonbuild/compilers/objcpp.py
@@ -6,6 +6,7 @@
 import typing as T
 
 from .. import coredata
+from .. import options
 from ..mesonlib import OptionKey
 
 from .mixins.clike import CLikeCompiler
@@ -80,7 +81,7 @@
     def get_options(self) -> coredata.MutableKeyedOptionDictType:
         return self.update_options(
             super().get_options(),
-            self.create_option(coredata.UserComboOption,
+            self.create_option(options.UserComboOption,
                                OptionKey('std', machine=self.for_machine, lang='cpp'),
                                'C++ language standard to use',
                                ['none', 'c++98', 'c++11', 'c++14', 'c++17', 'c++20', 'c++2b',
diff --git a/mesonbuild/compilers/rust.py b/mesonbuild/compilers/rust.py
index ce10791..0f52dbb 100644
--- a/mesonbuild/compilers/rust.py
+++ b/mesonbuild/compilers/rust.py
@@ -9,7 +9,7 @@
 import re
 import typing as T
 
-from .. import coredata
+from .. import options
 from ..mesonlib import EnvironmentException, MesonException, Popen_safe_logged, OptionKey
 from .compilers import Compiler, clike_debug_args
 
@@ -158,7 +158,7 @@
     # use_linker_args method instead.
 
     def get_options(self) -> MutableKeyedOptionDictType:
-        return dict((self.create_option(coredata.UserComboOption,
+        return dict((self.create_option(options.UserComboOption,
                                         OptionKey('std', machine=self.for_machine, lang=self.language),
                                         'Rust edition to use',
                                         ['none', '2015', '2018', '2021'],
diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py
index 6e67587..c6c932f 100644
--- a/mesonbuild/coredata.py
+++ b/mesonbuild/coredata.py
@@ -6,7 +6,7 @@
 
 import copy
 
-from . import mlog, mparser
+from . import mlog, mparser, options
 import pickle, os, uuid
 import sys
 from itertools import chain
@@ -82,15 +82,6 @@
     stable_version_array[-2] = str(int(stable_version_array[-2]) + 1)
     stable_version = '.'.join(stable_version_array)
 
-backendlist = ['ninja', 'vs', 'vs2010', 'vs2012', 'vs2013', 'vs2015', 'vs2017', 'vs2019', 'vs2022', 'xcode', 'none']
-genvslitelist = ['vs2022']
-buildtypelist = ['plain', 'debug', 'debugoptimized', 'release', 'minsize', 'custom']
-
-DEFAULT_YIELDING = False
-
-# Can't bind this near the class method it seems, sadly.
-_T = T.TypeVar('_T')
-
 
 def get_genvs_default_buildtype_list() -> list[str]:
     # just debug, debugoptimized, and release for now
@@ -108,312 +99,6 @@
         self.current_version = current_version
 
 
-class UserOption(T.Generic[_T], HoldableObject):
-    def __init__(self, name: str, description: str, choices: T.Optional[T.Union[str, T.List[_T]]],
-                 yielding: bool,
-                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
-        super().__init__()
-        self.name = name
-        self.choices = choices
-        self.description = description
-        if not isinstance(yielding, bool):
-            raise MesonException('Value of "yielding" must be a boolean.')
-        self.yielding = yielding
-        self.deprecated = deprecated
-        self.readonly = False
-
-    def listify(self, value: T.Any) -> T.List[T.Any]:
-        return [value]
-
-    def printable_value(self) -> T.Union[str, int, bool, T.List[T.Union[str, int, bool]]]:
-        assert isinstance(self.value, (str, int, bool, list))
-        return self.value
-
-    # Check that the input is a valid value and return the
-    # "cleaned" or "native" version. For example the Boolean
-    # option could take the string "true" and return True.
-    def validate_value(self, value: T.Any) -> _T:
-        raise RuntimeError('Derived option class did not override validate_value.')
-
-    def set_value(self, newvalue: T.Any) -> bool:
-        oldvalue = getattr(self, 'value', None)
-        self.value = self.validate_value(newvalue)
-        return self.value != oldvalue
-
-class UserStringOption(UserOption[str]):
-    def __init__(self, name: str, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING,
-                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
-        super().__init__(name, description, None, yielding, deprecated)
-        self.set_value(value)
-
-    def validate_value(self, value: T.Any) -> str:
-        if not isinstance(value, str):
-            raise MesonException(f'The value of option "{self.name}" is "{value}", which is not a string.')
-        return value
-
-class UserBooleanOption(UserOption[bool]):
-    def __init__(self, name: str, description: str, value: bool, yielding: bool = DEFAULT_YIELDING,
-                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
-        super().__init__(name, description, [True, False], yielding, deprecated)
-        self.set_value(value)
-
-    def __bool__(self) -> bool:
-        return self.value
-
-    def validate_value(self, value: T.Any) -> bool:
-        if isinstance(value, bool):
-            return value
-        if not isinstance(value, str):
-            raise MesonException(f'Option "{self.name}" value {value} cannot be converted to a boolean')
-        if value.lower() == 'true':
-            return True
-        if value.lower() == 'false':
-            return False
-        raise MesonException(f'Option "{self.name}" value {value} is not boolean (true or false).')
-
-class UserIntegerOption(UserOption[int]):
-    def __init__(self, name: str, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING,
-                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
-        min_value, max_value, default_value = value
-        self.min_value = min_value
-        self.max_value = max_value
-        c: T.List[str] = []
-        if min_value is not None:
-            c.append('>=' + str(min_value))
-        if max_value is not None:
-            c.append('<=' + str(max_value))
-        choices = ', '.join(c)
-        super().__init__(name, description, choices, yielding, deprecated)
-        self.set_value(default_value)
-
-    def validate_value(self, value: T.Any) -> int:
-        if isinstance(value, str):
-            value = self.toint(value)
-        if not isinstance(value, int):
-            raise MesonException(f'Value {value!r} for option "{self.name}" is not an integer.')
-        if self.min_value is not None and value < self.min_value:
-            raise MesonException(f'Value {value} for option "{self.name}" is less than minimum value {self.min_value}.')
-        if self.max_value is not None and value > self.max_value:
-            raise MesonException(f'Value {value} for option "{self.name}" is more than maximum value {self.max_value}.')
-        return value
-
-    def toint(self, valuestring: str) -> int:
-        try:
-            return int(valuestring)
-        except ValueError:
-            raise MesonException(f'Value string "{valuestring}" for option "{self.name}" is not convertible to an integer.')
-
-class OctalInt(int):
-    # NinjaBackend.get_user_option_args uses str() to converts it to a command line option
-    # UserUmaskOption.toint() uses int(str, 8) to convert it to an integer
-    # So we need to use oct instead of dec here if we do not want values to be misinterpreted.
-    def __str__(self) -> str:
-        return oct(int(self))
-
-class UserUmaskOption(UserIntegerOption, UserOption[T.Union[str, OctalInt]]):
-    def __init__(self, name: str, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING,
-                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
-        super().__init__(name, description, (0, 0o777, value), yielding, deprecated)
-        self.choices = ['preserve', '0000-0777']
-
-    def printable_value(self) -> str:
-        if self.value == 'preserve':
-            return self.value
-        return format(self.value, '04o')
-
-    def validate_value(self, value: T.Any) -> T.Union[str, OctalInt]:
-        if value == 'preserve':
-            return 'preserve'
-        return OctalInt(super().validate_value(value))
-
-    def toint(self, valuestring: T.Union[str, OctalInt]) -> int:
-        try:
-            return int(valuestring, 8)
-        except ValueError as e:
-            raise MesonException(f'Invalid mode for option "{self.name}" {e}')
-
-class UserComboOption(UserOption[str]):
-    def __init__(self, name: str, description: str, choices: T.List[str], value: T.Any,
-                 yielding: bool = DEFAULT_YIELDING,
-                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
-        super().__init__(name, description, choices, yielding, deprecated)
-        if not isinstance(self.choices, list):
-            raise MesonException(f'Combo choices for option "{self.name}" must be an array.')
-        for i in self.choices:
-            if not isinstance(i, str):
-                raise MesonException(f'Combo choice elements for option "{self.name}" must be strings.')
-        self.set_value(value)
-
-    def validate_value(self, value: T.Any) -> str:
-        if value not in self.choices:
-            if isinstance(value, bool):
-                _type = 'boolean'
-            elif isinstance(value, (int, float)):
-                _type = 'number'
-            else:
-                _type = 'string'
-            optionsstring = ', '.join([f'"{item}"' for item in self.choices])
-            raise MesonException('Value "{}" (of type "{}") for option "{}" is not one of the choices.'
-                                 ' Possible choices are (as string): {}.'.format(
-                                     value, _type, self.name, optionsstring))
-        return value
-
-class UserArrayOption(UserOption[T.List[str]]):
-    def __init__(self, name: str, description: str, value: T.Union[str, T.List[str]],
-                 split_args: bool = False,
-                 allow_dups: bool = False, yielding: bool = DEFAULT_YIELDING,
-                 choices: T.Optional[T.List[str]] = None,
-                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
-        super().__init__(name, description, choices if choices is not None else [], yielding, deprecated)
-        self.split_args = split_args
-        self.allow_dups = allow_dups
-        self.set_value(value)
-
-    def listify(self, value: T.Any) -> T.List[T.Any]:
-        try:
-            return listify_array_value(value, self.split_args)
-        except MesonException as e:
-            raise MesonException(f'error in option "{self.name}": {e!s}')
-
-    def validate_value(self, value: T.Union[str, T.List[str]]) -> T.List[str]:
-        newvalue = self.listify(value)
-
-        if not self.allow_dups and len(set(newvalue)) != len(newvalue):
-            msg = 'Duplicated values in array option is deprecated. ' \
-                  'This will become a hard error in the future.'
-            mlog.deprecation(msg)
-        for i in newvalue:
-            if not isinstance(i, str):
-                raise MesonException(f'String array element "{newvalue!s}" for option "{self.name}" is not a string.')
-        if self.choices:
-            bad = [x for x in newvalue if x not in self.choices]
-            if bad:
-                raise MesonException('Value{} "{}" for option "{}" {} not in allowed choices: "{}"'.format(
-                    '' if len(bad) == 1 else 's',
-                    ', '.join(bad),
-                    self.name,
-                    'is' if len(bad) == 1 else 'are',
-                    ', '.join(self.choices))
-                )
-        return newvalue
-
-    def extend_value(self, value: T.Union[str, T.List[str]]) -> None:
-        """Extend the value with an additional value."""
-        new = self.validate_value(value)
-        self.set_value(self.value + new)
-
-
-class UserFeatureOption(UserComboOption):
-    static_choices = ['enabled', 'disabled', 'auto']
-
-    def __init__(self, name: str, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING,
-                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
-        super().__init__(name, description, self.static_choices, value, yielding, deprecated)
-        self.name: T.Optional[str] = None  # TODO: Refactor options to all store their name
-
-    def is_enabled(self) -> bool:
-        return self.value == 'enabled'
-
-    def is_disabled(self) -> bool:
-        return self.value == 'disabled'
-
-    def is_auto(self) -> bool:
-        return self.value == 'auto'
-
-class UserStdOption(UserComboOption):
-    '''
-    UserOption specific to c_std and cpp_std options. User can set a list of
-    STDs in preference order and it selects the first one supported by current
-    compiler.
-
-    For historical reasons, some compilers (msvc) allowed setting a GNU std and
-    silently fell back to C std. This is now deprecated. Projects that support
-    both GNU and MSVC compilers should set e.g. c_std=gnu11,c11.
-
-    This is not using self.deprecated mechanism we already have for project
-    options because we want to print a warning if ALL values are deprecated, not
-    if SOME values are deprecated.
-    '''
-    def __init__(self, lang: str, all_stds: T.List[str]) -> None:
-        self.lang = lang.lower()
-        self.all_stds = ['none'] + all_stds
-        # Map a deprecated std to its replacement. e.g. gnu11 -> c11.
-        self.deprecated_stds: T.Dict[str, str] = {}
-        opt_name = 'cpp_std' if lang == 'c++' else f'{lang}_std'
-        super().__init__(opt_name, f'{lang} language standard to use', ['none'], 'none')
-
-    def set_versions(self, versions: T.List[str], gnu: bool = False, gnu_deprecated: bool = False) -> None:
-        assert all(std in self.all_stds for std in versions)
-        self.choices += versions
-        if gnu:
-            gnu_stds_map = {f'gnu{std[1:]}': std for std in versions}
-            if gnu_deprecated:
-                self.deprecated_stds.update(gnu_stds_map)
-            else:
-                self.choices += gnu_stds_map.keys()
-
-    def validate_value(self, value: T.Union[str, T.List[str]]) -> str:
-        try:
-            candidates = listify_array_value(value)
-        except MesonException as e:
-            raise MesonException(f'error in option "{self.name}": {e!s}')
-        unknown = ','.join(std for std in candidates if std not in self.all_stds)
-        if unknown:
-            raise MesonException(f'Unknown option "{self.name}" value {unknown}. Possible values are {self.all_stds}.')
-        # Check first if any of the candidates are not deprecated
-        for std in candidates:
-            if std in self.choices:
-                return std
-        # Fallback to a deprecated std if any
-        for std in candidates:
-            newstd = self.deprecated_stds.get(std)
-            if newstd is not None:
-                mlog.deprecation(
-                    f'None of the values {candidates} are supported by the {self.lang} compiler.\n' +
-                    f'However, the deprecated {std} std currently falls back to {newstd}.\n' +
-                    'This will be an error in the future.\n' +
-                    'If the project supports both GNU and MSVC compilers, a value such as\n' +
-                    '"c_std=gnu11,c11" specifies that GNU is preferred but it can safely fallback to plain c11.')
-                return newstd
-        raise MesonException(f'None of values {candidates} are supported by the {self.lang.upper()} compiler. ' +
-                             f'Possible values for option "{self.name}" are {self.choices}')
-
-@dataclass
-class OptionsView(abc.Mapping):
-    '''A view on an options dictionary for a given subproject and with overrides.
-    '''
-
-    # TODO: the typing here could be made more explicit using a TypeDict from
-    # python 3.8 or typing_extensions
-    options: KeyedOptionDictType
-    subproject: T.Optional[str] = None
-    overrides: T.Optional[T.Mapping[OptionKey, T.Union[str, int, bool, T.List[str]]]] = None
-
-    def __getitem__(self, key: OptionKey) -> UserOption:
-        # FIXME: This is fundamentally the same algorithm than interpreter.get_option_internal().
-        # We should try to share the code somehow.
-        key = key.evolve(subproject=self.subproject)
-        if not key.is_project():
-            opt = self.options.get(key)
-            if opt is None or opt.yielding:
-                opt = self.options[key.as_root()]
-        else:
-            opt = self.options[key]
-            if opt.yielding:
-                opt = self.options.get(key.as_root(), opt)
-        if self.overrides:
-            override_value = self.overrides.get(key.as_root())
-            if override_value is not None:
-                opt = copy.copy(opt)
-                opt.set_value(override_value)
-        return opt
-
-    def __iter__(self) -> T.Iterator[OptionKey]:
-        return iter(self.options)
-
-    def __len__(self) -> int:
-        return len(self.options)
-
 class DependencyCacheType(enum.Enum):
 
     OTHER = 0
@@ -664,7 +349,7 @@
         # getting the "system default" is always wrong on multiarch
         # platforms as it gets a value like lib/x86_64-linux-gnu.
         if self.cross_files:
-            BUILTIN_OPTIONS[OptionKey('libdir')].default = 'lib'
+            options.BUILTIN_OPTIONS[OptionKey('libdir')].default = 'lib'
 
     def sanitize_prefix(self, prefix: str) -> str:
         prefix = os.path.expanduser(prefix)
@@ -698,7 +383,7 @@
         except TypeError:
             return value
         if option.name.endswith('dir') and value.is_absolute() and \
-           option not in BUILTIN_DIR_NOPREFIX_OPTIONS:
+           option not in options.BUILTIN_DIR_NOPREFIX_OPTIONS:
             try:
                 # Try to relativize the path.
                 value = value.relative_to(prefix)
@@ -717,10 +402,10 @@
 
     def init_builtins(self, subproject: str) -> None:
         # Create builtin options with default values
-        for key, opt in BUILTIN_OPTIONS.items():
+        for key, opt in options.BUILTIN_OPTIONS.items():
             self.add_builtin_option(self.options, key.evolve(subproject=subproject), opt)
         for for_machine in iter(MachineChoice):
-            for key, opt in BUILTIN_OPTIONS_PER_MACHINE.items():
+            for key, opt in options.BUILTIN_OPTIONS_PER_MACHINE.items():
                 self.add_builtin_option(self.options, key.evolve(subproject=subproject, machine=for_machine), opt)
 
     @staticmethod
@@ -737,13 +422,13 @@
 
     def init_backend_options(self, backend_name: str) -> None:
         if backend_name == 'ninja':
-            self.options[OptionKey('backend_max_links')] = UserIntegerOption(
+            self.options[OptionKey('backend_max_links')] = options.UserIntegerOption(
                 'backend_max_links',
                 'Maximum number of linker processes to run or 0 for no '
                 'limit',
                 (0, None, 0))
         elif backend_name.startswith('vs'):
-            self.options[OptionKey('backend_startup_project')] = UserStringOption(
+            self.options[OptionKey('backend_startup_project')] = options.UserStringOption(
                 'backend_startup_project',
                 'Default project to execute in Visual Studio',
                 '')
@@ -881,7 +566,7 @@
 
     @staticmethod
     def is_per_machine_option(optname: OptionKey) -> bool:
-        if optname.as_host() in BUILTIN_OPTIONS_PER_MACHINE:
+        if optname.as_host() in options.BUILTIN_OPTIONS_PER_MACHINE:
             return True
         return optname.lang is not None
 
@@ -930,7 +615,7 @@
     def copy_build_options_from_regular_ones(self) -> bool:
         dirty = False
         assert not self.is_cross_build()
-        for k in BUILTIN_OPTIONS_PER_MACHINE:
+        for k in options.BUILTIN_OPTIONS_PER_MACHINE:
             o = self.options[k]
             dirty |= self.options[k.as_build()].set_value(o.value)
         for bk, bv in self.options.items():
@@ -944,21 +629,21 @@
 
         return dirty
 
-    def set_options(self, options: T.Dict[OptionKey, T.Any], subproject: str = '', first_invocation: bool = False) -> bool:
+    def set_options(self, opts_to_set: T.Dict[OptionKey, T.Any], subproject: str = '', first_invocation: bool = False) -> bool:
         dirty = False
         if not self.is_cross_build():
-            options = {k: v for k, v in options.items() if k.machine is not MachineChoice.BUILD}
+            opts_to_set = {k: v for k, v in opts_to_set.items() if k.machine is not MachineChoice.BUILD}
         # Set prefix first because it's needed to sanitize other options
         pfk = OptionKey('prefix')
-        if pfk in options:
-            prefix = self.sanitize_prefix(options[pfk])
+        if pfk in opts_to_set:
+            prefix = self.sanitize_prefix(opts_to_set[pfk])
             dirty |= self.options[OptionKey('prefix')].set_value(prefix)
-            for key in BUILTIN_DIR_NOPREFIX_OPTIONS:
-                if key not in options:
-                    dirty |= self.options[key].set_value(BUILTIN_OPTIONS[key].prefixed_default(key, prefix))
+            for key in options.BUILTIN_DIR_NOPREFIX_OPTIONS:
+                if key not in opts_to_set:
+                    dirty |= self.options[key].set_value(options.BUILTIN_OPTIONS[key].prefixed_default(key, prefix))
 
         unknown_options: T.List[OptionKey] = []
-        for k, v in options.items():
+        for k, v in opts_to_set.items():
             if k == pfk:
                 continue
             elif k in self.options:
@@ -1255,9 +940,9 @@
 
 
 def register_builtin_arguments(parser: argparse.ArgumentParser) -> None:
-    for n, b in BUILTIN_OPTIONS.items():
+    for n, b in options.BUILTIN_OPTIONS.items():
         b.add_to_argparse(str(n), parser, '')
-    for n, b in BUILTIN_OPTIONS_PER_MACHINE.items():
+    for n, b in options.BUILTIN_OPTIONS_PER_MACHINE.items():
         b.add_to_argparse(str(n), parser, ' (just for host machine)')
         b.add_to_argparse(str(n.as_build()), parser, ' (just for build machine)')
     parser.add_argument('-D', action='append', dest='projectoptions', default=[], metavar="option",
@@ -1281,9 +966,9 @@
 
     # Merge builtin options set with --option into the dict.
     for key in chain(
-            BUILTIN_OPTIONS.keys(),
-            (k.as_build() for k in BUILTIN_OPTIONS_PER_MACHINE.keys()),
-            BUILTIN_OPTIONS_PER_MACHINE.keys(),
+            options.BUILTIN_OPTIONS.keys(),
+            (k.as_build() for k in options.BUILTIN_OPTIONS_PER_MACHINE.keys()),
+            options.BUILTIN_OPTIONS_PER_MACHINE.keys(),
     ):
         name = str(key)
         value = getattr(args, name, None)
@@ -1295,171 +980,41 @@
             args.cmd_line_options[key] = value
             delattr(args, name)
 
+@dataclass
+class OptionsView(abc.Mapping):
+    '''A view on an options dictionary for a given subproject and with overrides.
+    '''
 
-_U = T.TypeVar('_U', bound=UserOption[_T])
+    # TODO: the typing here could be made more explicit using a TypeDict from
+    # python 3.8 or typing_extensions
+    options: KeyedOptionDictType
+    subproject: T.Optional[str] = None
+    overrides: T.Optional[T.Mapping[OptionKey, T.Union[str, int, bool, T.List[str]]]] = None
 
-class BuiltinOption(T.Generic[_T, _U]):
-
-    """Class for a builtin option type.
-
-    There are some cases that are not fully supported yet.
-    """
-
-    def __init__(self, opt_type: T.Type[_U], description: str, default: T.Any, yielding: bool = True, *,
-                 choices: T.Any = None, readonly: bool = False):
-        self.opt_type = opt_type
-        self.description = description
-        self.default = default
-        self.choices = choices
-        self.yielding = yielding
-        self.readonly = readonly
-
-    def init_option(self, name: 'OptionKey', value: T.Optional[T.Any], prefix: str) -> _U:
-        """Create an instance of opt_type and return it."""
-        if value is None:
-            value = self.prefixed_default(name, prefix)
-        keywords = {'yielding': self.yielding, 'value': value}
-        if self.choices:
-            keywords['choices'] = self.choices
-        o = self.opt_type(name.name, self.description, **keywords)
-        o.readonly = self.readonly
-        return o
-
-    def _argparse_action(self) -> T.Optional[str]:
-        # If the type is a boolean, the presence of the argument in --foo form
-        # is to enable it. Disabling happens by using -Dfoo=false, which is
-        # parsed under `args.projectoptions` and does not hit this codepath.
-        if isinstance(self.default, bool):
-            return 'store_true'
-        return None
-
-    def _argparse_choices(self) -> T.Any:
-        if self.opt_type is UserBooleanOption:
-            return [True, False]
-        elif self.opt_type is UserFeatureOption:
-            return UserFeatureOption.static_choices
-        return self.choices
-
-    @staticmethod
-    def argparse_name_to_arg(name: str) -> str:
-        if name == 'warning_level':
-            return '--warnlevel'
+    def __getitem__(self, key: OptionKey) -> UserOption:
+        # FIXME: This is fundamentally the same algorithm than interpreter.get_option_internal().
+        # We should try to share the code somehow.
+        key = key.evolve(subproject=self.subproject)
+        if not key.is_project():
+            opt = self.options.get(key)
+            if opt is None or opt.yielding:
+                opt = self.options[key.as_root()]
         else:
-            return '--' + name.replace('_', '-')
+            opt = self.options[key]
+            if opt.yielding:
+                opt = self.options.get(key.as_root(), opt)
+        if self.overrides:
+            override_value = self.overrides.get(key.as_root())
+            if override_value is not None:
+                opt = copy.copy(opt)
+                opt.set_value(override_value)
+        return opt
 
-    def prefixed_default(self, name: 'OptionKey', prefix: str = '') -> T.Any:
-        if self.opt_type in [UserComboOption, UserIntegerOption]:
-            return self.default
-        try:
-            return BUILTIN_DIR_NOPREFIX_OPTIONS[name][prefix]
-        except KeyError:
-            pass
-        return self.default
+    def __iter__(self) -> T.Iterator[OptionKey]:
+        return iter(self.options)
 
-    def add_to_argparse(self, name: str, parser: argparse.ArgumentParser, help_suffix: str) -> None:
-        kwargs = OrderedDict()
-
-        c = self._argparse_choices()
-        b = self._argparse_action()
-        h = self.description
-        if not b:
-            h = '{} (default: {}).'.format(h.rstrip('.'), self.prefixed_default(name))
-        else:
-            kwargs['action'] = b
-        if c and not b:
-            kwargs['choices'] = c
-        kwargs['default'] = argparse.SUPPRESS
-        kwargs['dest'] = name
-
-        cmdline_name = self.argparse_name_to_arg(name)
-        parser.add_argument(cmdline_name, help=h + help_suffix, **kwargs)
-
-
-# Update `docs/markdown/Builtin-options.md` after changing the options below
-# Also update mesonlib._BUILTIN_NAMES. See the comment there for why this is required.
-# Please also update completion scripts in $MESONSRC/data/shell-completions/
-BUILTIN_DIR_OPTIONS: T.Dict['OptionKey', 'BuiltinOption'] = OrderedDict([
-    (OptionKey('prefix'),          BuiltinOption(UserStringOption, 'Installation prefix', default_prefix())),
-    (OptionKey('bindir'),          BuiltinOption(UserStringOption, 'Executable directory', 'bin')),
-    (OptionKey('datadir'),         BuiltinOption(UserStringOption, 'Data file directory', default_datadir())),
-    (OptionKey('includedir'),      BuiltinOption(UserStringOption, 'Header file directory', default_includedir())),
-    (OptionKey('infodir'),         BuiltinOption(UserStringOption, 'Info page directory', default_infodir())),
-    (OptionKey('libdir'),          BuiltinOption(UserStringOption, 'Library directory', default_libdir())),
-    (OptionKey('licensedir'),      BuiltinOption(UserStringOption, 'Licenses directory', '')),
-    (OptionKey('libexecdir'),      BuiltinOption(UserStringOption, 'Library executable directory', default_libexecdir())),
-    (OptionKey('localedir'),       BuiltinOption(UserStringOption, 'Locale data directory', default_localedir())),
-    (OptionKey('localstatedir'),   BuiltinOption(UserStringOption, 'Localstate data directory', 'var')),
-    (OptionKey('mandir'),          BuiltinOption(UserStringOption, 'Manual page directory', default_mandir())),
-    (OptionKey('sbindir'),         BuiltinOption(UserStringOption, 'System executable directory', default_sbindir())),
-    (OptionKey('sharedstatedir'),  BuiltinOption(UserStringOption, 'Architecture-independent data directory', 'com')),
-    (OptionKey('sysconfdir'),      BuiltinOption(UserStringOption, 'Sysconf data directory', default_sysconfdir())),
-])
-
-BUILTIN_CORE_OPTIONS: T.Dict['OptionKey', 'BuiltinOption'] = OrderedDict([
-    (OptionKey('auto_features'),   BuiltinOption(UserFeatureOption, "Override value of all 'auto' features", 'auto')),
-    (OptionKey('backend'),         BuiltinOption(UserComboOption, 'Backend to use', 'ninja', choices=backendlist,
-                                                 readonly=True)),
-    (OptionKey('genvslite'),
-     BuiltinOption(
-         UserComboOption,
-         'Setup multiple buildtype-suffixed ninja-backend build directories, '
-         'and a [builddir]_vs containing a Visual Studio meta-backend with multiple configurations that calls into them',
-         'vs2022',
-         choices=genvslitelist)
-     ),
-    (OptionKey('buildtype'),       BuiltinOption(UserComboOption, 'Build type to use', 'debug',
-                                                 choices=buildtypelist)),
-    (OptionKey('debug'),           BuiltinOption(UserBooleanOption, 'Enable debug symbols and other information', True)),
-    (OptionKey('default_library'), BuiltinOption(UserComboOption, 'Default library type', 'shared', choices=['shared', 'static', 'both'],
-                                                 yielding=False)),
-    (OptionKey('errorlogs'),       BuiltinOption(UserBooleanOption, "Whether to print the logs from failing tests", True)),
-    (OptionKey('install_umask'),   BuiltinOption(UserUmaskOption, 'Default umask to apply on permissions of installed files', '022')),
-    (OptionKey('layout'),          BuiltinOption(UserComboOption, 'Build directory layout', 'mirror', choices=['mirror', 'flat'])),
-    (OptionKey('optimization'),    BuiltinOption(UserComboOption, 'Optimization level', '0', choices=['plain', '0', 'g', '1', '2', '3', 's'])),
-    (OptionKey('prefer_static'),   BuiltinOption(UserBooleanOption, 'Whether to try static linking before shared linking', False)),
-    (OptionKey('stdsplit'),        BuiltinOption(UserBooleanOption, 'Split stdout and stderr in test logs', True)),
-    (OptionKey('strip'),           BuiltinOption(UserBooleanOption, 'Strip targets on install', False)),
-    (OptionKey('unity'),           BuiltinOption(UserComboOption, 'Unity build', 'off', choices=['on', 'off', 'subprojects'])),
-    (OptionKey('unity_size'),      BuiltinOption(UserIntegerOption, 'Unity block size', (2, None, 4))),
-    (OptionKey('warning_level'),   BuiltinOption(UserComboOption, 'Compiler warning level to use', '1', choices=['0', '1', '2', '3', 'everything'], yielding=False)),
-    (OptionKey('werror'),          BuiltinOption(UserBooleanOption, 'Treat warnings as errors', False, yielding=False)),
-    (OptionKey('wrap_mode'),       BuiltinOption(UserComboOption, 'Wrap mode', 'default', choices=['default', 'nofallback', 'nodownload', 'forcefallback', 'nopromote'])),
-    (OptionKey('force_fallback_for'), BuiltinOption(UserArrayOption, 'Force fallback for those subprojects', [])),
-    (OptionKey('vsenv'),           BuiltinOption(UserBooleanOption, 'Activate Visual Studio environment', False, readonly=True)),
-
-    # Pkgconfig module
-    (OptionKey('relocatable', module='pkgconfig'),
-     BuiltinOption(UserBooleanOption, 'Generate pkgconfig files as relocatable', False)),
-
-    # Python module
-    (OptionKey('bytecompile', module='python'),
-     BuiltinOption(UserIntegerOption, 'Whether to compile bytecode', (-1, 2, 0))),
-    (OptionKey('install_env', module='python'),
-     BuiltinOption(UserComboOption, 'Which python environment to install to', 'prefix', choices=['auto', 'prefix', 'system', 'venv'])),
-    (OptionKey('platlibdir', module='python'),
-     BuiltinOption(UserStringOption, 'Directory for site-specific, platform-specific files.', '')),
-    (OptionKey('purelibdir', module='python'),
-     BuiltinOption(UserStringOption, 'Directory for site-specific, non-platform-specific files.', '')),
-    (OptionKey('allow_limited_api', module='python'),
-     BuiltinOption(UserBooleanOption, 'Whether to allow use of the Python Limited API', True)),
-])
-
-BUILTIN_OPTIONS = OrderedDict(chain(BUILTIN_DIR_OPTIONS.items(), BUILTIN_CORE_OPTIONS.items()))
-
-BUILTIN_OPTIONS_PER_MACHINE: T.Dict['OptionKey', 'BuiltinOption'] = OrderedDict([
-    (OptionKey('pkg_config_path'), BuiltinOption(UserArrayOption, 'List of additional paths for pkg-config to search', [])),
-    (OptionKey('cmake_prefix_path'), BuiltinOption(UserArrayOption, 'List of additional prefixes for cmake to search', [])),
-])
-
-# Special prefix-dependent defaults for installation directories that reside in
-# a path outside of the prefix in FHS and common usage.
-BUILTIN_DIR_NOPREFIX_OPTIONS: T.Dict[OptionKey, T.Dict[str, str]] = {
-    OptionKey('sysconfdir'):     {'/usr': '/etc'},
-    OptionKey('localstatedir'):  {'/usr': '/var',     '/usr/local': '/var/local'},
-    OptionKey('sharedstatedir'): {'/usr': '/var/lib', '/usr/local': '/var/local/lib'},
-    OptionKey('platlibdir', module='python'): {},
-    OptionKey('purelibdir', module='python'): {},
-}
+    def __len__(self) -> int:
+        return len(self.options)
 
 FORBIDDEN_TARGET_NAMES = frozenset({
     'clean',
diff --git a/mesonbuild/interpreter/compiler.py b/mesonbuild/interpreter/compiler.py
index 50a850a..359e8e7 100644
--- a/mesonbuild/interpreter/compiler.py
+++ b/mesonbuild/interpreter/compiler.py
@@ -13,6 +13,7 @@
 from .. import build
 from .. import coredata
 from .. import dependencies
+from .. import options
 from .. import mesonlib
 from .. import mlog
 from ..compilers import SUFFIX_TO_LANG, RunResult
@@ -89,7 +90,7 @@
         header_include_directories: T.List[build.IncludeDirs]
         header_no_builtin_args: bool
         header_prefix: str
-        header_required: T.Union[bool, coredata.UserFeatureOption]
+        header_required: T.Union[bool, options.UserFeatureOption]
 
     class PreprocessKW(TypedDict):
         output: str
@@ -685,7 +686,7 @@
     @typed_pos_args('compiler.find_library', str)
     @typed_kwargs(
         'compiler.find_library',
-        KwargInfo('required', (bool, coredata.UserFeatureOption), default=True),
+        KwargInfo('required', (bool, options.UserFeatureOption), default=True),
         KwargInfo('has_headers', ContainerTypeInfo(list, str), listify=True, default=[], since='0.50.0'),
         KwargInfo('static', (bool, NoneType), since='0.51.0'),
         KwargInfo('disabler', bool, default=False, since='0.49.0'),
diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py
index 50780ba..eb6783c 100644
--- a/mesonbuild/interpreter/interpreter.py
+++ b/mesonbuild/interpreter/interpreter.py
@@ -11,6 +11,7 @@
 from .. import coredata
 from .. import dependencies
 from .. import mlog
+from .. import options
 from .. import build
 from .. import optinterpreter
 from .. import compilers
@@ -163,7 +164,7 @@
                 elif isinstance(i, Disabler):
                     FeatureNew.single_use('disabler in summary', '0.64.0', subproject)
                     formatted_values.append(mlog.red('NO'))
-                elif isinstance(i, coredata.UserOption):
+                elif isinstance(i, options.UserOption):
                     FeatureNew.single_use('feature option in summary', '0.58.0', subproject)
                     formatted_values.append(i.printable_value())
                 else:
@@ -450,7 +451,7 @@
             build.StructuredSources: OBJ.StructuredSourcesHolder,
             compilers.RunResult: compilerOBJ.TryRunResultHolder,
             dependencies.ExternalLibrary: OBJ.ExternalLibraryHolder,
-            coredata.UserFeatureOption: OBJ.FeatureOptionHolder,
+            options.UserFeatureOption: OBJ.FeatureOptionHolder,
             envconfig.MachineInfo: OBJ.MachineHolder,
             build.ConfigurationData: OBJ.ConfigurationDataHolder,
         })
@@ -1047,7 +1048,7 @@
                 # FIXME: Are there other files used by cargo interpreter?
                 [os.path.join(subdir, 'Cargo.toml')])
 
-    def get_option_internal(self, optname: str) -> coredata.UserOption:
+    def get_option_internal(self, optname: str) -> options.UserOption:
         key = OptionKey.from_string(optname).evolve(subproject=self.subproject)
 
         if not key.is_project():
@@ -1056,7 +1057,7 @@
                 if v is None or v.yielding:
                     v = opts.get(key.as_root())
                 if v is not None:
-                    assert isinstance(v, coredata.UserOption), 'for mypy'
+                    assert isinstance(v, options.UserOption), 'for mypy'
                     return v
 
         try:
@@ -1085,7 +1086,7 @@
     @typed_pos_args('get_option', str)
     @noKwargs
     def func_get_option(self, nodes: mparser.BaseNode, args: T.Tuple[str],
-                        kwargs: 'TYPE_kwargs') -> T.Union[coredata.UserOption, 'TYPE_var']:
+                        kwargs: 'TYPE_kwargs') -> T.Union[options.UserOption, 'TYPE_var']:
         optname = args[0]
         if ':' in optname:
             raise InterpreterException('Having a colon in option name is forbidden, '
@@ -1096,10 +1097,10 @@
             raise InterpreterException(f'Invalid option name {optname!r}')
 
         opt = self.get_option_internal(optname)
-        if isinstance(opt, coredata.UserFeatureOption):
+        if isinstance(opt, options.UserFeatureOption):
             opt.name = optname
             return opt
-        elif isinstance(opt, coredata.UserOption):
+        elif isinstance(opt, options.UserOption):
             if isinstance(opt.value, str):
                 return P_OBJ.OptionString(opt.value, f'{{{optname}}}')
             return opt.value
diff --git a/mesonbuild/interpreter/interpreterobjects.py b/mesonbuild/interpreter/interpreterobjects.py
index 8cd9a2b..8e2eace 100644
--- a/mesonbuild/interpreter/interpreterobjects.py
+++ b/mesonbuild/interpreter/interpreterobjects.py
@@ -9,6 +9,7 @@
 
 from .. import mesonlib
 from .. import coredata
+from .. import options
 from .. import build
 from .. import mlog
 
@@ -52,7 +53,7 @@
     disabled = False
     required = False
     feature: T.Optional[str] = None
-    if isinstance(val, coredata.UserFeatureOption):
+    if isinstance(val, options.UserFeatureOption):
         if not feature_check:
             feature_check = FeatureNew('User option "feature"', '0.47.0')
         feature_check.use(subproject)
@@ -85,12 +86,12 @@
             raise InvalidCode(f'Search directory {d} is not an absolute path.')
     return [str(s) for s in search_dirs]
 
-class FeatureOptionHolder(ObjectHolder[coredata.UserFeatureOption]):
-    def __init__(self, option: coredata.UserFeatureOption, interpreter: 'Interpreter'):
+class FeatureOptionHolder(ObjectHolder[options.UserFeatureOption]):
+    def __init__(self, option: options.UserFeatureOption, interpreter: 'Interpreter'):
         super().__init__(option, interpreter)
         if option and option.is_auto():
             # TODO: we need to cast here because options is not a TypedDict
-            auto = T.cast('coredata.UserFeatureOption', self.env.coredata.options[OptionKey('auto_features')])
+            auto = T.cast('options.UserFeatureOption', self.env.coredata.options[OptionKey('auto_features')])
             self.held_object = copy.copy(auto)
             self.held_object.name = option.name
         self.methods.update({'enabled': self.enabled_method,
@@ -108,12 +109,12 @@
     def value(self) -> str:
         return 'disabled' if not self.held_object else self.held_object.value
 
-    def as_disabled(self) -> coredata.UserFeatureOption:
+    def as_disabled(self) -> options.UserFeatureOption:
         disabled = copy.deepcopy(self.held_object)
         disabled.value = 'disabled'
         return disabled
 
-    def as_enabled(self) -> coredata.UserFeatureOption:
+    def as_enabled(self) -> options.UserFeatureOption:
         enabled = copy.deepcopy(self.held_object)
         enabled.value = 'enabled'
         return enabled
@@ -139,7 +140,7 @@
     def auto_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool:
         return self.value == 'auto'
 
-    def _disable_if(self, condition: bool, message: T.Optional[str]) -> coredata.UserFeatureOption:
+    def _disable_if(self, condition: bool, message: T.Optional[str]) -> options.UserFeatureOption:
         if not condition:
             return copy.deepcopy(self.held_object)
 
@@ -156,7 +157,7 @@
         'feature_option.require',
         _ERROR_MSG_KW,
     )
-    def require_method(self, args: T.Tuple[bool], kwargs: 'kwargs.FeatureOptionRequire') -> coredata.UserFeatureOption:
+    def require_method(self, args: T.Tuple[bool], kwargs: 'kwargs.FeatureOptionRequire') -> options.UserFeatureOption:
         return self._disable_if(not args[0], kwargs['error_message'])
 
     @FeatureNew('feature_option.disable_if()', '1.1.0')
@@ -165,7 +166,7 @@
         'feature_option.disable_if',
         _ERROR_MSG_KW,
     )
-    def disable_if_method(self, args: T.Tuple[bool], kwargs: 'kwargs.FeatureOptionRequire') -> coredata.UserFeatureOption:
+    def disable_if_method(self, args: T.Tuple[bool], kwargs: 'kwargs.FeatureOptionRequire') -> options.UserFeatureOption:
         return self._disable_if(args[0], kwargs['error_message'])
 
     @FeatureNew('feature_option.enable_if()', '1.1.0')
@@ -174,7 +175,7 @@
         'feature_option.enable_if',
         _ERROR_MSG_KW,
     )
-    def enable_if_method(self, args: T.Tuple[bool], kwargs: 'kwargs.FeatureOptionRequire') -> coredata.UserFeatureOption:
+    def enable_if_method(self, args: T.Tuple[bool], kwargs: 'kwargs.FeatureOptionRequire') -> options.UserFeatureOption:
         if not args[0]:
             return copy.deepcopy(self.held_object)
 
@@ -188,13 +189,13 @@
     @FeatureNew('feature_option.disable_auto_if()', '0.59.0')
     @noKwargs
     @typed_pos_args('feature_option.disable_auto_if', bool)
-    def disable_auto_if_method(self, args: T.Tuple[bool], kwargs: TYPE_kwargs) -> coredata.UserFeatureOption:
+    def disable_auto_if_method(self, args: T.Tuple[bool], kwargs: TYPE_kwargs) -> options.UserFeatureOption:
         return copy.deepcopy(self.held_object) if self.value != 'auto' or not args[0] else self.as_disabled()
 
     @FeatureNew('feature_option.enable_auto_if()', '1.1.0')
     @noKwargs
     @typed_pos_args('feature_option.enable_auto_if', bool)
-    def enable_auto_if_method(self, args: T.Tuple[bool], kwargs: TYPE_kwargs) -> coredata.UserFeatureOption:
+    def enable_auto_if_method(self, args: T.Tuple[bool], kwargs: TYPE_kwargs) -> options.UserFeatureOption:
         return self.as_enabled() if self.value == 'auto' and args[0] else copy.deepcopy(self.held_object)
 
 
diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py
index 17f7876..e41827b 100644
--- a/mesonbuild/interpreter/kwargs.py
+++ b/mesonbuild/interpreter/kwargs.py
@@ -11,6 +11,7 @@
 
 from .. import build
 from .. import coredata
+from .. import options
 from ..compilers import Compiler
 from ..dependencies.base import Dependency
 from ..mesonlib import EnvironmentVariables, MachineChoice, File, FileMode, FileOrString, OptionKey
@@ -73,7 +74,7 @@
     a boolean or a feature option should inherit it's arguments from this class.
     """
 
-    required: T.Union[bool, coredata.UserFeatureOption]
+    required: T.Union[bool, options.UserFeatureOption]
 
 
 class ExtractSearchDirs(TypedDict):
diff --git a/mesonbuild/interpreter/type_checking.py b/mesonbuild/interpreter/type_checking.py
index 9b7e35c..2856136 100644
--- a/mesonbuild/interpreter/type_checking.py
+++ b/mesonbuild/interpreter/type_checking.py
@@ -11,7 +11,7 @@
 from ..build import (CustomTarget, BuildTarget,
                      CustomTargetIndex, ExtractedObjects, GeneratedList, IncludeDirs,
                      BothLibraries, SharedLibrary, StaticLibrary, Jar, Executable, StructuredSources)
-from ..coredata import UserFeatureOption
+from ..options import UserFeatureOption
 from ..dependencies import Dependency, InternalDependency
 from ..interpreterbase.decorators import KwargInfo, ContainerTypeInfo
 from ..mesonlib import (File, FileMode, MachineChoice, listify, has_path_sep,
diff --git a/mesonbuild/interpreterbase/helpers.py b/mesonbuild/interpreterbase/helpers.py
index 3942f2c..0b04362 100644
--- a/mesonbuild/interpreterbase/helpers.py
+++ b/mesonbuild/interpreterbase/helpers.py
@@ -5,7 +5,7 @@
 
 from .. import mesonlib, mparser
 from .exceptions import InterpreterException, InvalidArguments
-from ..coredata import UserOption
+from ..options import UserOption
 
 
 import collections.abc
diff --git a/mesonbuild/mconf.py b/mesonbuild/mconf.py
index 2cef24f..4a572a0 100644
--- a/mesonbuild/mconf.py
+++ b/mesonbuild/mconf.py
@@ -14,6 +14,7 @@
 
 from . import build
 from . import coredata
+from . import options
 from . import environment
 from . import mesonlib
 from . import mintro
@@ -223,18 +224,18 @@
         self._add_line(mlog.normal_yellow(section + ':'), '', '', '')
         self.print_margin = 2
 
-    def print_options(self, title: str, options: 'coredata.KeyedOptionDictType') -> None:
-        if not options:
+    def print_options(self, title: str, opts: 'coredata.KeyedOptionDictType') -> None:
+        if not opts:
             return
         if title:
             self.add_title(title)
-        auto = T.cast('coredata.UserFeatureOption', self.coredata.options[OptionKey('auto_features')])
-        for k, o in sorted(options.items()):
+        auto = T.cast('options.UserFeatureOption', self.coredata.options[OptionKey('auto_features')])
+        for k, o in sorted(opts.items()):
             printable_value = o.printable_value()
             root = k.as_root()
             if o.yielding and k.subproject and root in self.coredata.options:
                 printable_value = '<inherited from main project>'
-            if isinstance(o, coredata.UserFeatureOption) and o.is_auto():
+            if isinstance(o, options.UserFeatureOption) and o.is_auto():
                 printable_value = auto.printable_value()
             self.add_option(str(root), o.description, printable_value, o.choices)
 
@@ -255,7 +256,7 @@
         if not self.default_values_only:
             mlog.log('  Build dir ', self.build_dir)
 
-        dir_option_names = set(coredata.BUILTIN_DIR_OPTIONS)
+        dir_option_names = set(options.BUILTIN_DIR_OPTIONS)
         test_option_names = {OptionKey('errorlogs'),
                              OptionKey('stdsplit')}
 
diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py
index bdbb59e..a5ce72a 100644
--- a/mesonbuild/mintro.py
+++ b/mesonbuild/mintro.py
@@ -19,7 +19,7 @@
 import sys
 import typing as T
 
-from . import build, mesonlib, coredata as cdata
+from . import build, mesonlib, options, coredata as cdata
 from .ast import IntrospectionInterpreter, BUILD_TARGET_FUNCTIONS, AstConditionLevel, AstIDGenerator, AstIndentationGenerator, AstJSONPrinter
 from .backend import backends
 from .dependencies import Dependency
@@ -88,7 +88,7 @@
         flag = '--' + key.replace('_', '-')
         parser.add_argument(flag, action='store_true', dest=key, default=False, help=val.desc)
 
-    parser.add_argument('--backend', choices=sorted(cdata.backendlist), dest='backend', default='ninja',
+    parser.add_argument('--backend', choices=sorted(options.backendlist), dest='backend', default='ninja',
                         help='The backend to use for the --buildoptions introspection.')
     parser.add_argument('-a', '--all', action='store_true', dest='all', default=False,
                         help='Print all available information.')
@@ -284,7 +284,7 @@
     optlist: T.List[T.Dict[str, T.Union[str, bool, int, T.List[str]]]] = []
     subprojects = subprojects or []
 
-    dir_option_names = set(cdata.BUILTIN_DIR_OPTIONS)
+    dir_option_names = set(options.BUILTIN_DIR_OPTIONS)
     test_option_names = {OptionKey('errorlogs'),
                          OptionKey('stdsplit')}
 
@@ -302,20 +302,20 @@
                 for s in subprojects:
                     core_options[k.evolve(subproject=s)] = v
 
-    def add_keys(options: 'cdata.KeyedOptionDictType', section: str) -> None:
-        for key, opt in sorted(options.items()):
+    def add_keys(opts: 'cdata.KeyedOptionDictType', section: str) -> None:
+        for key, opt in sorted(opts.items()):
             optdict = {'name': str(key), 'value': opt.value, 'section': section,
                        'machine': key.machine.get_lower_case_name() if coredata.is_per_machine_option(key) else 'any'}
-            if isinstance(opt, cdata.UserStringOption):
+            if isinstance(opt, options.UserStringOption):
                 typestr = 'string'
-            elif isinstance(opt, cdata.UserBooleanOption):
+            elif isinstance(opt, options.UserBooleanOption):
                 typestr = 'boolean'
-            elif isinstance(opt, cdata.UserComboOption):
+            elif isinstance(opt, options.UserComboOption):
                 optdict['choices'] = opt.choices
                 typestr = 'combo'
-            elif isinstance(opt, cdata.UserIntegerOption):
+            elif isinstance(opt, options.UserIntegerOption):
                 typestr = 'integer'
-            elif isinstance(opt, cdata.UserArrayOption):
+            elif isinstance(opt, options.UserArrayOption):
                 typestr = 'array'
                 if opt.choices:
                     optdict['choices'] = opt.choices
diff --git a/mesonbuild/modules/_qt.py b/mesonbuild/modules/_qt.py
index 7effa1f..1b9b21e 100644
--- a/mesonbuild/modules/_qt.py
+++ b/mesonbuild/modules/_qt.py
@@ -12,6 +12,7 @@
 from . import ModuleReturnValue, ExtensionModule
 from .. import build
 from .. import coredata
+from .. import options
 from .. import mlog
 from ..dependencies import find_external_dependency, Dependency, ExternalLibrary, InternalDependency
 from ..mesonlib import MesonException, File, version_compare, Popen_safe
@@ -256,7 +257,7 @@
     @noPosargs
     @typed_kwargs(
         'qt.has_tools',
-        KwargInfo('required', (bool, coredata.UserFeatureOption), default=False),
+        KwargInfo('required', (bool, options.UserFeatureOption), default=False),
         KwargInfo('method', str, default='auto'),
     )
     def has_tools(self, state: 'ModuleState', args: T.Tuple, kwargs: 'HasToolKwArgs') -> bool:
diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py
index ebe0d92..1a73070 100644
--- a/mesonbuild/modules/pkgconfig.py
+++ b/mesonbuild/modules/pkgconfig.py
@@ -14,7 +14,7 @@
 from .. import dependencies
 from .. import mesonlib
 from .. import mlog
-from ..coredata import BUILTIN_DIR_OPTIONS
+from ..options import BUILTIN_DIR_OPTIONS
 from ..dependencies.pkgconfig import PkgConfigDependency, PkgConfigInterface
 from ..interpreter.type_checking import D_MODULE_VERSIONS_KW, INSTALL_DIR_KW, VARIABLES_KW, NoneType
 from ..interpreterbase import FeatureNew, FeatureDeprecated
diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py
index 59b5050..d195a3f 100644
--- a/mesonbuild/modules/python.py
+++ b/mesonbuild/modules/python.py
@@ -9,7 +9,7 @@
 from . import ExtensionModule, ModuleInfo
 from .. import mesonlib
 from .. import mlog
-from ..coredata import UserFeatureOption
+from ..options import UserFeatureOption
 from ..build import known_shmod_kwargs, CustomTarget, CustomTargetIndex, BuildTarget, GeneratedList, StructuredSources, ExtractedObjects, SharedModule
 from ..dependencies import NotFoundDependency
 from ..dependencies.detect import get_dep_identifier, find_external_dependency
diff --git a/mesonbuild/optinterpreter.py b/mesonbuild/optinterpreter.py
index 599da65..02c49e0 100644
--- a/mesonbuild/optinterpreter.py
+++ b/mesonbuild/optinterpreter.py
@@ -7,6 +7,7 @@
 import typing as T
 
 from . import coredata
+from . import options
 from . import mesonlib
 from . import mparser
 from . import mlog
@@ -66,7 +67,7 @@
     def __init__(self, subproject: 'SubProject') -> None:
         self.options: 'coredata.MutableKeyedOptionDictType' = {}
         self.subproject = subproject
-        self.option_types: T.Dict[str, T.Callable[..., coredata.UserOption]] = {
+        self.option_types: T.Dict[str, T.Callable[..., options.UserOption]] = {
             'string': self.string_parser,
             'boolean': self.boolean_parser,
             'combo': self.combo_parser,
@@ -179,7 +180,7 @@
             since='0.60.0',
             since_values={str: '0.63.0'},
         ),
-        KwargInfo('yield', bool, default=coredata.DEFAULT_YIELDING, since='0.45.0'),
+        KwargInfo('yield', bool, default=options.DEFAULT_YIELDING, since='0.45.0'),
         allow_unknown=True,
     )
     @typed_pos_args('option', str)
@@ -208,8 +209,8 @@
         'string option',
         KwargInfo('value', str, default=''),
     )
-    def string_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: StringArgs) -> coredata.UserOption:
-        return coredata.UserStringOption(name, description, kwargs['value'], *args)
+    def string_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: StringArgs) -> options.UserOption:
+        return options.UserStringOption(name, description, kwargs['value'], *args)
 
     @typed_kwargs(
         'boolean option',
@@ -221,20 +222,20 @@
             deprecated_values={str: ('1.1.0', 'use a boolean, not a string')},
         ),
     )
-    def boolean_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: BooleanArgs) -> coredata.UserOption:
-        return coredata.UserBooleanOption(name, description, kwargs['value'], *args)
+    def boolean_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: BooleanArgs) -> options.UserOption:
+        return options.UserBooleanOption(name, description, kwargs['value'], *args)
 
     @typed_kwargs(
         'combo option',
         KwargInfo('value', (str, NoneType)),
         KwargInfo('choices', ContainerTypeInfo(list, str, allow_empty=False), required=True),
     )
-    def combo_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: ComboArgs) -> coredata.UserOption:
+    def combo_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: ComboArgs) -> options.UserOption:
         choices = kwargs['choices']
         value = kwargs['value']
         if value is None:
             value = kwargs['choices'][0]
-        return coredata.UserComboOption(name, description, choices, value, *args)
+        return options.UserComboOption(name, description, choices, value, *args)
 
     @typed_kwargs(
         'integer option',
@@ -248,17 +249,17 @@
         KwargInfo('min', (int, NoneType)),
         KwargInfo('max', (int, NoneType)),
     )
-    def integer_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: IntegerArgs) -> coredata.UserOption:
+    def integer_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: IntegerArgs) -> options.UserOption:
         value = kwargs['value']
         inttuple = (kwargs['min'], kwargs['max'], value)
-        return coredata.UserIntegerOption(name, description, inttuple, *args)
+        return options.UserIntegerOption(name, description, inttuple, *args)
 
     @typed_kwargs(
         'string array option',
         KwargInfo('value', (ContainerTypeInfo(list, str), str, NoneType)),
         KwargInfo('choices', ContainerTypeInfo(list, str), default=[]),
     )
-    def string_array_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: StringArrayArgs) -> coredata.UserOption:
+    def string_array_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: StringArrayArgs) -> options.UserOption:
         choices = kwargs['choices']
         value = kwargs['value'] if kwargs['value'] is not None else choices
         if isinstance(value, str):
@@ -266,7 +267,7 @@
                 FeatureDeprecated('String value for array option', '1.3.0').use(self.subproject)
             else:
                 raise mesonlib.MesonException('Value does not define an array: ' + value)
-        return coredata.UserArrayOption(name, description, value,
+        return options.UserArrayOption(name, description, value,
                                         choices=choices,
                                         yielding=args[0],
                                         deprecated=args[1])
@@ -275,5 +276,5 @@
         'feature option',
         KwargInfo('value', str, default='auto', validator=in_set_validator({'auto', 'enabled', 'disabled'})),
     )
-    def feature_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: FeatureArgs) -> coredata.UserOption:
-        return coredata.UserFeatureOption(name, description, kwargs['value'], *args)
+    def feature_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: FeatureArgs) -> options.UserOption:
+        return options.UserFeatureOption(name, description, kwargs['value'], *args)
diff --git a/mesonbuild/options.py b/mesonbuild/options.py
new file mode 100644
index 0000000..0a20539
--- /dev/null
+++ b/mesonbuild/options.py
@@ -0,0 +1,480 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2013-2024 Contributors to the The Meson project
+
+from dataclasses import dataclass
+from collections import OrderedDict, abc
+from itertools import chain
+import argparse
+
+from .mesonlib import (
+    HoldableObject,
+    OptionKey,
+    default_prefix,
+    default_datadir,
+    default_includedir,
+    default_datadir,
+    default_infodir,
+    default_libdir,
+    default_libexecdir,
+    default_localedir,
+    default_mandir,
+    default_sbindir,
+    default_sysconfdir,
+    MesonException,
+    listify_array_value,
+)
+
+import typing as T
+
+DEFAULT_YIELDING = False
+
+# Can't bind this near the class method it seems, sadly.
+_T = T.TypeVar('_T')
+
+backendlist = ['ninja', 'vs', 'vs2010', 'vs2012', 'vs2013', 'vs2015', 'vs2017', 'vs2019', 'vs2022', 'xcode', 'none']
+genvslitelist = ['vs2022']
+buildtypelist = ['plain', 'debug', 'debugoptimized', 'release', 'minsize', 'custom']
+
+
+class UserOption(T.Generic[_T], HoldableObject):
+    def __init__(self, name: str, description: str, choices: T.Optional[T.Union[str, T.List[_T]]],
+                 yielding: bool,
+                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
+        super().__init__()
+        self.name = name
+        self.choices = choices
+        self.description = description
+        if not isinstance(yielding, bool):
+            raise MesonException('Value of "yielding" must be a boolean.')
+        self.yielding = yielding
+        self.deprecated = deprecated
+        self.readonly = False
+
+    def listify(self, value: T.Any) -> T.List[T.Any]:
+        return [value]
+
+    def printable_value(self) -> T.Union[str, int, bool, T.List[T.Union[str, int, bool]]]:
+        assert isinstance(self.value, (str, int, bool, list))
+        return self.value
+
+    # Check that the input is a valid value and return the
+    # "cleaned" or "native" version. For example the Boolean
+    # option could take the string "true" and return True.
+    def validate_value(self, value: T.Any) -> _T:
+        raise RuntimeError('Derived option class did not override validate_value.')
+
+    def set_value(self, newvalue: T.Any) -> bool:
+        oldvalue = getattr(self, 'value', None)
+        self.value = self.validate_value(newvalue)
+        return self.value != oldvalue
+
+_U = T.TypeVar('_U', bound=UserOption[_T])
+
+
+class UserStringOption(UserOption[str]):
+    def __init__(self, name: str, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING,
+                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
+        super().__init__(name, description, None, yielding, deprecated)
+        self.set_value(value)
+
+    def validate_value(self, value: T.Any) -> str:
+        if not isinstance(value, str):
+            raise MesonException(f'The value of option "{self.name}" is "{value}", which is not a string.')
+        return value
+
+class UserBooleanOption(UserOption[bool]):
+    def __init__(self, name: str, description: str, value: bool, yielding: bool = DEFAULT_YIELDING,
+                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
+        super().__init__(name, description, [True, False], yielding, deprecated)
+        self.set_value(value)
+
+    def __bool__(self) -> bool:
+        return self.value
+
+    def validate_value(self, value: T.Any) -> bool:
+        if isinstance(value, bool):
+            return value
+        if not isinstance(value, str):
+            raise MesonException(f'Option "{self.name}" value {value} cannot be converted to a boolean')
+        if value.lower() == 'true':
+            return True
+        if value.lower() == 'false':
+            return False
+        raise MesonException(f'Option "{self.name}" value {value} is not boolean (true or false).')
+
+class UserIntegerOption(UserOption[int]):
+    def __init__(self, name: str, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING,
+                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
+        min_value, max_value, default_value = value
+        self.min_value = min_value
+        self.max_value = max_value
+        c: T.List[str] = []
+        if min_value is not None:
+            c.append('>=' + str(min_value))
+        if max_value is not None:
+            c.append('<=' + str(max_value))
+        choices = ', '.join(c)
+        super().__init__(name, description, choices, yielding, deprecated)
+        self.set_value(default_value)
+
+    def validate_value(self, value: T.Any) -> int:
+        if isinstance(value, str):
+            value = self.toint(value)
+        if not isinstance(value, int):
+            raise MesonException(f'Value {value!r} for option "{self.name}" is not an integer.')
+        if self.min_value is not None and value < self.min_value:
+            raise MesonException(f'Value {value} for option "{self.name}" is less than minimum value {self.min_value}.')
+        if self.max_value is not None and value > self.max_value:
+            raise MesonException(f'Value {value} for option "{self.name}" is more than maximum value {self.max_value}.')
+        return value
+
+    def toint(self, valuestring: str) -> int:
+        try:
+            return int(valuestring)
+        except ValueError:
+            raise MesonException(f'Value string "{valuestring}" for option "{self.name}" is not convertible to an integer.')
+
+class OctalInt(int):
+    # NinjaBackend.get_user_option_args uses str() to converts it to a command line option
+    # UserUmaskOption.toint() uses int(str, 8) to convert it to an integer
+    # So we need to use oct instead of dec here if we do not want values to be misinterpreted.
+    def __str__(self) -> str:
+        return oct(int(self))
+
+class UserUmaskOption(UserIntegerOption, UserOption[T.Union[str, OctalInt]]):
+    def __init__(self, name: str, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING,
+                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
+        super().__init__(name, description, (0, 0o777, value), yielding, deprecated)
+        self.choices = ['preserve', '0000-0777']
+
+    def printable_value(self) -> str:
+        if self.value == 'preserve':
+            return self.value
+        return format(self.value, '04o')
+
+    def validate_value(self, value: T.Any) -> T.Union[str, OctalInt]:
+        if value == 'preserve':
+            return 'preserve'
+        return OctalInt(super().validate_value(value))
+
+    def toint(self, valuestring: T.Union[str, OctalInt]) -> int:
+        try:
+            return int(valuestring, 8)
+        except ValueError as e:
+            raise MesonException(f'Invalid mode for option "{self.name}" {e}')
+
+class UserComboOption(UserOption[str]):
+    def __init__(self, name: str, description: str, choices: T.List[str], value: T.Any,
+                 yielding: bool = DEFAULT_YIELDING,
+                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
+        super().__init__(name, description, choices, yielding, deprecated)
+        if not isinstance(self.choices, list):
+            raise MesonException(f'Combo choices for option "{self.name}" must be an array.')
+        for i in self.choices:
+            if not isinstance(i, str):
+                raise MesonException(f'Combo choice elements for option "{self.name}" must be strings.')
+        self.set_value(value)
+
+    def validate_value(self, value: T.Any) -> str:
+        if value not in self.choices:
+            if isinstance(value, bool):
+                _type = 'boolean'
+            elif isinstance(value, (int, float)):
+                _type = 'number'
+            else:
+                _type = 'string'
+            optionsstring = ', '.join([f'"{item}"' for item in self.choices])
+            raise MesonException('Value "{}" (of type "{}") for option "{}" is not one of the choices.'
+                                 ' Possible choices are (as string): {}.'.format(
+                                     value, _type, self.name, optionsstring))
+        return value
+
+class UserArrayOption(UserOption[T.List[str]]):
+    def __init__(self, name: str, description: str, value: T.Union[str, T.List[str]],
+                 split_args: bool = False,
+                 allow_dups: bool = False, yielding: bool = DEFAULT_YIELDING,
+                 choices: T.Optional[T.List[str]] = None,
+                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
+        super().__init__(name, description, choices if choices is not None else [], yielding, deprecated)
+        self.split_args = split_args
+        self.allow_dups = allow_dups
+        self.set_value(value)
+
+    def listify(self, value: T.Any) -> T.List[T.Any]:
+        try:
+            return listify_array_value(value, self.split_args)
+        except MesonException as e:
+            raise MesonException(f'error in option "{self.name}": {e!s}')
+
+    def validate_value(self, value: T.Union[str, T.List[str]]) -> T.List[str]:
+        newvalue = self.listify(value)
+
+        if not self.allow_dups and len(set(newvalue)) != len(newvalue):
+            msg = 'Duplicated values in array option is deprecated. ' \
+                  'This will become a hard error in the future.'
+            mlog.deprecation(msg)
+        for i in newvalue:
+            if not isinstance(i, str):
+                raise MesonException(f'String array element "{newvalue!s}" for option "{self.name}" is not a string.')
+        if self.choices:
+            bad = [x for x in newvalue if x not in self.choices]
+            if bad:
+                raise MesonException('Value{} "{}" for option "{}" {} not in allowed choices: "{}"'.format(
+                    '' if len(bad) == 1 else 's',
+                    ', '.join(bad),
+                    self.name,
+                    'is' if len(bad) == 1 else 'are',
+                    ', '.join(self.choices))
+                )
+        return newvalue
+
+    def extend_value(self, value: T.Union[str, T.List[str]]) -> None:
+        """Extend the value with an additional value."""
+        new = self.validate_value(value)
+        self.set_value(self.value + new)
+
+
+class UserFeatureOption(UserComboOption):
+    static_choices = ['enabled', 'disabled', 'auto']
+
+    def __init__(self, name: str, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING,
+                 deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False):
+        super().__init__(name, description, self.static_choices, value, yielding, deprecated)
+        self.name: T.Optional[str] = None  # TODO: Refactor options to all store their name
+
+    def is_enabled(self) -> bool:
+        return self.value == 'enabled'
+
+    def is_disabled(self) -> bool:
+        return self.value == 'disabled'
+
+    def is_auto(self) -> bool:
+        return self.value == 'auto'
+
+class UserStdOption(UserComboOption):
+    '''
+    UserOption specific to c_std and cpp_std options. User can set a list of
+    STDs in preference order and it selects the first one supported by current
+    compiler.
+
+    For historical reasons, some compilers (msvc) allowed setting a GNU std and
+    silently fell back to C std. This is now deprecated. Projects that support
+    both GNU and MSVC compilers should set e.g. c_std=gnu11,c11.
+
+    This is not using self.deprecated mechanism we already have for project
+    options because we want to print a warning if ALL values are deprecated, not
+    if SOME values are deprecated.
+    '''
+    def __init__(self, lang: str, all_stds: T.List[str]) -> None:
+        self.lang = lang.lower()
+        self.all_stds = ['none'] + all_stds
+        # Map a deprecated std to its replacement. e.g. gnu11 -> c11.
+        self.deprecated_stds: T.Dict[str, str] = {}
+        opt_name = 'cpp_std' if lang == 'c++' else f'{lang}_std'
+        super().__init__(opt_name, f'{lang} language standard to use', ['none'], 'none')
+
+    def set_versions(self, versions: T.List[str], gnu: bool = False, gnu_deprecated: bool = False) -> None:
+        assert all(std in self.all_stds for std in versions)
+        self.choices += versions
+        if gnu:
+            gnu_stds_map = {f'gnu{std[1:]}': std for std in versions}
+            if gnu_deprecated:
+                self.deprecated_stds.update(gnu_stds_map)
+            else:
+                self.choices += gnu_stds_map.keys()
+
+    def validate_value(self, value: T.Union[str, T.List[str]]) -> str:
+        try:
+            candidates = listify_array_value(value)
+        except MesonException as e:
+            raise MesonException(f'error in option "{self.name}": {e!s}')
+        unknown = ','.join(std for std in candidates if std not in self.all_stds)
+        if unknown:
+            raise MesonException(f'Unknown option "{self.name}" value {unknown}. Possible values are {self.all_stds}.')
+        # Check first if any of the candidates are not deprecated
+        for std in candidates:
+            if std in self.choices:
+                return std
+        # Fallback to a deprecated std if any
+        for std in candidates:
+            newstd = self.deprecated_stds.get(std)
+            if newstd is not None:
+                mlog.deprecation(
+                    f'None of the values {candidates} are supported by the {self.lang} compiler.\n' +
+                    f'However, the deprecated {std} std currently falls back to {newstd}.\n' +
+                    'This will be an error in the future.\n' +
+                    'If the project supports both GNU and MSVC compilers, a value such as\n' +
+                    '"c_std=gnu11,c11" specifies that GNU is preferred but it can safely fallback to plain c11.')
+                return newstd
+        raise MesonException(f'None of values {candidates} are supported by the {self.lang.upper()} compiler. ' +
+                             f'Possible values for option "{self.name}" are {self.choices}')
+
+
+class BuiltinOption(T.Generic[_T, _U]):
+
+    """Class for a builtin option type.
+
+    There are some cases that are not fully supported yet.
+    """
+
+    def __init__(self, opt_type: T.Type[_U], description: str, default: T.Any, yielding: bool = True, *,
+                 choices: T.Any = None, readonly: bool = False):
+        self.opt_type = opt_type
+        self.description = description
+        self.default = default
+        self.choices = choices
+        self.yielding = yielding
+        self.readonly = readonly
+
+    def init_option(self, name: 'OptionKey', value: T.Optional[T.Any], prefix: str) -> _U:
+        """Create an instance of opt_type and return it."""
+        if value is None:
+            value = self.prefixed_default(name, prefix)
+        keywords = {'yielding': self.yielding, 'value': value}
+        if self.choices:
+            keywords['choices'] = self.choices
+        o = self.opt_type(name.name, self.description, **keywords)
+        o.readonly = self.readonly
+        return o
+
+    def _argparse_action(self) -> T.Optional[str]:
+        # If the type is a boolean, the presence of the argument in --foo form
+        # is to enable it. Disabling happens by using -Dfoo=false, which is
+        # parsed under `args.projectoptions` and does not hit this codepath.
+        if isinstance(self.default, bool):
+            return 'store_true'
+        return None
+
+    def _argparse_choices(self) -> T.Any:
+        if self.opt_type is UserBooleanOption:
+            return [True, False]
+        elif self.opt_type is UserFeatureOption:
+            return UserFeatureOption.static_choices
+        return self.choices
+
+    @staticmethod
+    def argparse_name_to_arg(name: str) -> str:
+        if name == 'warning_level':
+            return '--warnlevel'
+        else:
+            return '--' + name.replace('_', '-')
+
+    def prefixed_default(self, name: 'OptionKey', prefix: str = '') -> T.Any:
+        if self.opt_type in [UserComboOption, UserIntegerOption]:
+            return self.default
+        try:
+            return BUILTIN_DIR_NOPREFIX_OPTIONS[name][prefix]
+        except KeyError:
+            pass
+        return self.default
+
+    def add_to_argparse(self, name: str, parser: argparse.ArgumentParser, help_suffix: str) -> None:
+        kwargs = OrderedDict()
+
+        c = self._argparse_choices()
+        b = self._argparse_action()
+        h = self.description
+        if not b:
+            h = '{} (default: {}).'.format(h.rstrip('.'), self.prefixed_default(name))
+        else:
+            kwargs['action'] = b
+        if c and not b:
+            kwargs['choices'] = c
+        kwargs['default'] = argparse.SUPPRESS
+        kwargs['dest'] = name
+
+        cmdline_name = self.argparse_name_to_arg(name)
+        parser.add_argument(cmdline_name, help=h + help_suffix, **kwargs)
+
+
+# Update `docs/markdown/Builtin-options.md` after changing the options below
+# Also update mesonlib._BUILTIN_NAMES. See the comment there for why this is required.
+# Please also update completion scripts in $MESONSRC/data/shell-completions/
+BUILTIN_DIR_OPTIONS: T.Dict['OptionKey', 'BuiltinOption'] = OrderedDict([
+    (OptionKey('prefix'),          BuiltinOption(UserStringOption, 'Installation prefix', default_prefix())),
+    (OptionKey('bindir'),          BuiltinOption(UserStringOption, 'Executable directory', 'bin')),
+    (OptionKey('datadir'),         BuiltinOption(UserStringOption, 'Data file directory', default_datadir())),
+    (OptionKey('includedir'),      BuiltinOption(UserStringOption, 'Header file directory', default_includedir())),
+    (OptionKey('infodir'),         BuiltinOption(UserStringOption, 'Info page directory', default_infodir())),
+    (OptionKey('libdir'),          BuiltinOption(UserStringOption, 'Library directory', default_libdir())),
+    (OptionKey('licensedir'),      BuiltinOption(UserStringOption, 'Licenses directory', '')),
+    (OptionKey('libexecdir'),      BuiltinOption(UserStringOption, 'Library executable directory', default_libexecdir())),
+    (OptionKey('localedir'),       BuiltinOption(UserStringOption, 'Locale data directory', default_localedir())),
+    (OptionKey('localstatedir'),   BuiltinOption(UserStringOption, 'Localstate data directory', 'var')),
+    (OptionKey('mandir'),          BuiltinOption(UserStringOption, 'Manual page directory', default_mandir())),
+    (OptionKey('sbindir'),         BuiltinOption(UserStringOption, 'System executable directory', default_sbindir())),
+    (OptionKey('sharedstatedir'),  BuiltinOption(UserStringOption, 'Architecture-independent data directory', 'com')),
+    (OptionKey('sysconfdir'),      BuiltinOption(UserStringOption, 'Sysconf data directory', default_sysconfdir())),
+])
+
+BUILTIN_CORE_OPTIONS: T.Dict['OptionKey', 'BuiltinOption'] = OrderedDict([
+    (OptionKey('auto_features'),   BuiltinOption(UserFeatureOption, "Override value of all 'auto' features", 'auto')),
+    (OptionKey('backend'),         BuiltinOption(UserComboOption, 'Backend to use', 'ninja', choices=backendlist,
+                                                 readonly=True)),
+    (OptionKey('genvslite'),
+     BuiltinOption(
+         UserComboOption,
+         'Setup multiple buildtype-suffixed ninja-backend build directories, '
+         'and a [builddir]_vs containing a Visual Studio meta-backend with multiple configurations that calls into them',
+         'vs2022',
+         choices=genvslitelist)
+     ),
+    (OptionKey('buildtype'),       BuiltinOption(UserComboOption, 'Build type to use', 'debug',
+                                                 choices=buildtypelist)),
+    (OptionKey('debug'),           BuiltinOption(UserBooleanOption, 'Enable debug symbols and other information', True)),
+    (OptionKey('default_library'), BuiltinOption(UserComboOption, 'Default library type', 'shared', choices=['shared', 'static', 'both'],
+                                                 yielding=False)),
+    (OptionKey('errorlogs'),       BuiltinOption(UserBooleanOption, "Whether to print the logs from failing tests", True)),
+    (OptionKey('install_umask'),   BuiltinOption(UserUmaskOption, 'Default umask to apply on permissions of installed files', '022')),
+    (OptionKey('layout'),          BuiltinOption(UserComboOption, 'Build directory layout', 'mirror', choices=['mirror', 'flat'])),
+    (OptionKey('optimization'),    BuiltinOption(UserComboOption, 'Optimization level', '0', choices=['plain', '0', 'g', '1', '2', '3', 's'])),
+    (OptionKey('prefer_static'),   BuiltinOption(UserBooleanOption, 'Whether to try static linking before shared linking', False)),
+    (OptionKey('stdsplit'),        BuiltinOption(UserBooleanOption, 'Split stdout and stderr in test logs', True)),
+    (OptionKey('strip'),           BuiltinOption(UserBooleanOption, 'Strip targets on install', False)),
+    (OptionKey('unity'),           BuiltinOption(UserComboOption, 'Unity build', 'off', choices=['on', 'off', 'subprojects'])),
+    (OptionKey('unity_size'),      BuiltinOption(UserIntegerOption, 'Unity block size', (2, None, 4))),
+    (OptionKey('warning_level'),   BuiltinOption(UserComboOption, 'Compiler warning level to use', '1', choices=['0', '1', '2', '3', 'everything'], yielding=False)),
+    (OptionKey('werror'),          BuiltinOption(UserBooleanOption, 'Treat warnings as errors', False, yielding=False)),
+    (OptionKey('wrap_mode'),       BuiltinOption(UserComboOption, 'Wrap mode', 'default', choices=['default', 'nofallback', 'nodownload', 'forcefallback', 'nopromote'])),
+    (OptionKey('force_fallback_for'), BuiltinOption(UserArrayOption, 'Force fallback for those subprojects', [])),
+    (OptionKey('vsenv'),           BuiltinOption(UserBooleanOption, 'Activate Visual Studio environment', False, readonly=True)),
+
+    # Pkgconfig module
+    (OptionKey('relocatable', module='pkgconfig'),
+     BuiltinOption(UserBooleanOption, 'Generate pkgconfig files as relocatable', False)),
+
+    # Python module
+    (OptionKey('bytecompile', module='python'),
+     BuiltinOption(UserIntegerOption, 'Whether to compile bytecode', (-1, 2, 0))),
+    (OptionKey('install_env', module='python'),
+     BuiltinOption(UserComboOption, 'Which python environment to install to', 'prefix', choices=['auto', 'prefix', 'system', 'venv'])),
+    (OptionKey('platlibdir', module='python'),
+     BuiltinOption(UserStringOption, 'Directory for site-specific, platform-specific files.', '')),
+    (OptionKey('purelibdir', module='python'),
+     BuiltinOption(UserStringOption, 'Directory for site-specific, non-platform-specific files.', '')),
+    (OptionKey('allow_limited_api', module='python'),
+     BuiltinOption(UserBooleanOption, 'Whether to allow use of the Python Limited API', True)),
+])
+
+BUILTIN_OPTIONS = OrderedDict(chain(BUILTIN_DIR_OPTIONS.items(), BUILTIN_CORE_OPTIONS.items()))
+
+BUILTIN_OPTIONS_PER_MACHINE: T.Dict['OptionKey', 'BuiltinOption'] = OrderedDict([
+    (OptionKey('pkg_config_path'), BuiltinOption(UserArrayOption, 'List of additional paths for pkg-config to search', [])),
+    (OptionKey('cmake_prefix_path'), BuiltinOption(UserArrayOption, 'List of additional prefixes for cmake to search', [])),
+])
+
+# Special prefix-dependent defaults for installation directories that reside in
+# a path outside of the prefix in FHS and common usage.
+BUILTIN_DIR_NOPREFIX_OPTIONS: T.Dict[OptionKey, T.Dict[str, str]] = {
+    OptionKey('sysconfdir'):     {'/usr': '/etc'},
+    OptionKey('localstatedir'):  {'/usr': '/var',     '/usr/local': '/var/local'},
+    OptionKey('sharedstatedir'): {'/usr': '/var/lib', '/usr/local': '/var/local/lib'},
+    OptionKey('platlibdir', module='python'): {},
+    OptionKey('purelibdir', module='python'): {},
+}
+
+
+class OptionStore:
+    def __init__(self):
+        # This class will hold all options for a given build directory
+        self.dummy = None
diff --git a/run_project_tests.py b/run_project_tests.py
index 23561d9..974273f 100755
--- a/run_project_tests.py
+++ b/run_project_tests.py
@@ -41,7 +41,8 @@
 from mesonbuild.build import ConfigurationData
 from mesonbuild.mesonlib import MachineChoice, Popen_safe, TemporaryDirectoryWinProof, setup_vsenv
 from mesonbuild.mlog import blue, bold, cyan, green, red, yellow, normal_green
-from mesonbuild.coredata import backendlist, version as meson_version
+from mesonbuild.coredata import version as meson_version
+from mesonbuild.options import backendlist
 from mesonbuild.modules.python import PythonExternalProgram
 from run_tests import (
     get_fake_options, run_configure, get_meson_script, get_backend_commands,
diff --git a/run_tests.py b/run_tests.py
index 6d33dd9..63eb62c 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -33,7 +33,8 @@
 from mesonbuild import mtest
 from mesonbuild import mlog
 from mesonbuild.environment import Environment, detect_ninja, detect_machine_info
-from mesonbuild.coredata import backendlist, version as meson_version
+from mesonbuild.coredata import version as meson_version
+from mesonbuild.options import backendlist
 from mesonbuild.mesonlib import OptionKey, setup_vsenv
 
 if T.TYPE_CHECKING:
diff --git a/test cases/unit/116 empty project/expected_mods.json b/test cases/unit/116 empty project/expected_mods.json
index 7463bcb..19f56a5 100644
--- a/test cases/unit/116 empty project/expected_mods.json
+++ b/test cases/unit/116 empty project/expected_mods.json
@@ -225,6 +225,7 @@
       "mesonbuild.mparser",
       "mesonbuild.msetup",
       "mesonbuild.optinterpreter",
+      "mesonbuild.options",
       "mesonbuild.programs",
       "mesonbuild.scripts",
       "mesonbuild.scripts.meson_exe",
@@ -237,6 +238,6 @@
       "mesonbuild.wrap",
       "mesonbuild.wrap.wrap"
     ],
-    "count": 68
+    "count": 69
   }
 }
diff --git a/unittests/datatests.py b/unittests/datatests.py
index b14bbac..19664e3 100644
--- a/unittests/datatests.py
+++ b/unittests/datatests.py
@@ -14,6 +14,7 @@
 import mesonbuild.envconfig
 import mesonbuild.environment
 import mesonbuild.coredata
+import mesonbuild.options
 import mesonbuild.modules.gnome
 from mesonbuild.interpreter import Interpreter
 from mesonbuild.ast import AstInterpreter
@@ -139,8 +140,8 @@
             found_entries |= options
 
         self.assertEqual(found_entries, {
-            *(str(k.evolve(module=None)) for k in mesonbuild.coredata.BUILTIN_OPTIONS),
-            *(str(k.evolve(module=None)) for k in mesonbuild.coredata.BUILTIN_OPTIONS_PER_MACHINE),
+            *(str(k.evolve(module=None)) for k in mesonbuild.options.BUILTIN_OPTIONS),
+            *(str(k.evolve(module=None)) for k in mesonbuild.options.BUILTIN_OPTIONS_PER_MACHINE),
         })
 
         # Check that `buildtype` table inside `Core options` matches how
diff --git a/unittests/platformagnostictests.py b/unittests/platformagnostictests.py
index ffc4b47..33a789b 100644
--- a/unittests/platformagnostictests.py
+++ b/unittests/platformagnostictests.py
@@ -274,7 +274,7 @@
             expected = json.load(f)['meson']['modules']
 
         self.assertEqual(data['modules'], expected)
-        self.assertEqual(data['count'], 68)
+        self.assertEqual(data['count'], 69)
 
     def test_meson_package_cache_dir(self):
         # Copy testdir into temporary directory to not pollute meson source tree.