Repository: mesonbuild/meson

Issue:
Separate parsing of cross file from downstream logic
This picks up where #3993 left off, doing for the rest of the file what that did for `[host_machine]`. Firstly, it is good engineering practice to have a clean pipeline by separating parsing from other logic (in code, if not at run time as interleaving can still be achieved via laziness). Secondly, this will do a large portion of the work of abstracting over the cross-vs-native-dependent portions of the code base. Third, this allows easier modifying of the surface syntax, as proposed in e.g. #3972 or #3282 

Here's an incomplete TOOD:

- [x] `[host_machine]` `[target_machine`] `MachineInfo` abstracts over cross file vs environment detection.
  - ~~#3993~~

- [x] `[binaries]` Should abstract over cross file vs environment variable vs default sources of truth. #4326 is convenient for the `PerMachine` abstraction which will allow us to conveniently store tools per machine. Unclear if due to dependence on other platform inference this can happen up front or it needs to happen lazily so as to take place after the former. `_get_compiler` in `environment.py` is a good guide. Unclear whether overlaps with the various lists compilers are stored.
  - ~~#4338~~
  - ~~#4445~~

- [x] `[properties]` Unfortunately defined as open-ended so not much we can do for structure. But we should use `PerMachine` to open the door to differentiating the build host and target machines' additional properties. The compiler options should all be added to per_machine option maps, so everything is taken from `coredata.compiler_options.*` and never `environment.properties` directly. #4626 and #4772 address this last bit.
  - ~~#4445~~
  - ~~#4626~~
  - ~~#4772~~

After all is complete, `self.cross_info` should no longer exist to make ad-hoc inspection impossible. This wasn't attempted in https://github.com/mesonbuild/meson/pull/4010 but in conjunction with that will finish the job of making cross not special. I actually now think it is better to do this before most of that, hence why I am writing this issue now.

CC @nirbheek 

Discussion:
Ericson2314: :crossed_fingers: this passes now. Also I set up pkgconfig to be cashed in cross and native alike.
Ericson2314: CC @dcbaker you would probably be interested in this since it refactors code from #4216.
Ericson2314: What's up with the swift test?
Ericson2314: Removed WIP because everything works but suspicious test and some unaddressed review comments.
Ericson2314: Now depends on #4580, #4581, broken out to debug this and make it slightly smaller.
dcbaker: Please let me know when you're ready for me to review again.
Ericson2314: @dcbaker what do you think about my responses to your points I didn't change? In particular, what do you think about the tension between `SafeConfigParser` for native files and the manual stuff for cross files?
Ericson2314: @dcbaker OK this should be ready for another round of review. (Even if CI breaks after these latest changes, I don't think it will be hard to fix.)
Ericson2314: @dcbaker one question for a follow-up PR is that if the native file also provides `[properties]` like the cross file, that would overlap with the `coredata`-based `get_external_args` and `get_external_link_args`.

I suspect the solution is to differentiate between -for-host and -for-build options on the coredata side too. There's no reason all project-side extra args should be discarded in the cross case. Discarding project flags for custom ones should be an extra choice in native and cross builds alike.
Ericson2314: @dcbaker ping :).
dcbaker: > I suspect the solution is to differentiate between -for-host and -for-build options on the coredata side too. There's no reason all project-side extra args should be discarded in the cross case. Discarding project flags for custom ones should be an extra choice in native and cross builds alike.

I think so as well. This likely means additional command line changes, as current I believe that -D$lang_args make this assumption. I'm not sure what the right approach there is.

dcbaker: I still have a few requests that haven't been handled or discussed, but I feel like this is getting pretty close and I like the changes overall, @jpakkane, @nirbheek, opinions?
Ericson2314: > I think so as well. This likely means additional command line changes, as current I believe that -D$lang_args make this assumption. I'm not sure what the right approach there is.

Awesome. https://github.com/mesonbuild/meson/pull/4626 did the opposite, but that was failing for pragmatic reason too. I started rewriting it last night to instead of the multiple `compiler_options` approach.
Ericson2314: Oooo likes like when I rebased a new test caught an issue. Should be fixed now.
dcbaker: @jpakkane I'd really like your feedback on whether you like the approach or not. I think it's nice and that it's going to make the native and cross file extensions we've been talking about easier to do. If you like the approach (even if you want some implementations changes) I'll base my work on this series.

@Ericson2314 I still have a few comments that are unresolved.
Ericson2314: @dcbaker I think the only remaining issue is @jpakkane cutting the annotation baby? :)

Patch:
diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py
index e8adc99de..22920f49b 100644
--- a/mesonbuild/backend/backends.py
+++ b/mesonbuild/backend/backends.py
@@ -343,8 +343,7 @@ class Backend:
                 exe_is_native = True
             is_cross_built = (not exe_is_native) and \
                 self.environment.is_cross_build() and \
-                self.environment.cross_info.need_cross_compiler() and \
-                self.environment.cross_info.need_exe_wrapper()
+                self.environment.need_exe_wrapper()
             if is_cross_built:
                 exe_wrapper = self.environment.get_exe_wrapper()
                 if not exe_wrapper.found():
@@ -640,11 +639,11 @@ class Backend:
     def get_mingw_extra_paths(self, target):
         paths = OrderedSet()
         # The cross bindir
-        root = self.environment.cross_info.get_root()
+        root = self.environment.properties.host.get_root()
         if root:
             paths.add(os.path.join(root, 'bin'))
         # The toolchain bindir
-        sys_root = self.environment.cross_info.get_sys_root()
+        sys_root = self.environment.properties.host.get_sys_root()
         if sys_root:
             paths.add(os.path.join(sys_root, 'bin'))
         # Get program and library dirs from all target compilers
@@ -693,8 +692,7 @@ class Backend:
             else:
                 cmd = [os.path.join(self.environment.get_build_dir(), self.get_target_filename(t.get_exe()))]
             is_cross = self.environment.is_cross_build() and \
-                self.environment.cross_info.need_cross_compiler() and \
-                self.environment.cross_info.need_exe_wrapper()
+                self.environment.need_exe_wrapper()
             if isinstance(exe, build.BuildTarget):
                 is_cross = is_cross and exe.is_cross
             if isinstance(exe, dependencies.ExternalProgram):
@@ -765,7 +763,7 @@ class Backend:
     def exe_object_to_cmd_array(self, exe):
         if self.environment.is_cross_build() and \
            isinstance(exe, build.BuildTarget) and exe.is_cross:
-            if self.environment.exe_wrapper is None and self.environment.cross_info.need_exe_wrapper():
+            if self.environment.exe_wrapper is None and self.environment.need_exe_wrapper():
                 s = textwrap.dedent('''
                     Can not use target {} as a generator because it is cross-built
                     and no exe wrapper is defined or needs_exe_wrapper is true.
@@ -979,15 +977,13 @@ class Backend:
     def create_install_data_files(self):
         install_data_file = os.path.join(self.environment.get_scratch_dir(), 'install.dat')
 
-        if self.environment.is_cross_build():
-            bins = self.environment.cross_info.config['binaries']
-            if 'strip' not in bins:
+        strip_bin = self.environment.binaries.host.lookup_entry('strip')
+        if strip_bin is None:
+            if self.environment.is_cross_build():
                 mlog.warning('Cross file does not specify strip binary, result will not be stripped.')
-                strip_bin = None
             else:
-                strip_bin = mesonlib.stringlistify(bins['strip'])
-        else:
-            strip_bin = self.environment.native_strip_bin
+                # TODO go through all candidates, like others
+                strip_bin = [self.environment.default_strip[0]]
         d = InstallData(self.environment.get_source_dir(),
                         self.environment.get_build_dir(),
                         self.environment.get_prefix(),
diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py
index 372d7c241..44bdaab55 100644
--- a/mesonbuild/backend/ninjabackend.py
+++ b/mesonbuild/backend/ninjabackend.py
@@ -536,8 +536,7 @@ int dummy;
         # a serialized executable wrapper for that and check if the
         # CustomTarget command needs extra paths first.
         is_cross = self.environment.is_cross_build() and \
-            self.environment.cross_info.need_cross_compiler() and \
-            self.environment.cross_info.need_exe_wrapper()
+            self.environment.need_exe_wrapper()
         if mesonlib.for_windows(is_cross, self.environment) or \
            mesonlib.for_cygwin(is_cross, self.environment):
             extra_bdeps = target.get_transitive_build_target_deps()
@@ -1338,7 +1337,7 @@ int dummy;
             if not is_cross:
                 self.generate_java_link(outfile)
         if is_cross:
-            if self.environment.cross_info.need_cross_compiler():
+            if self.environment.is_cross_build():
                 static_linker = self.build.static_cross_linker
             else:
                 static_linker = self.build.static_linker
@@ -1381,11 +1380,7 @@ int dummy;
         num_pools = self.environment.coredata.backend_options['backend_max_links'].value
         ctypes = [(self.build.compilers, False)]
         if self.environment.is_cross_build():
-            if self.environment.cross_info.need_cross_compiler():
-                ctypes.append((self.build.cross_compilers, True))
-            else:
-                # Native compiler masquerades as the cross compiler.
-                ctypes.append((self.build.compilers, True))
+            ctypes.append((self.build.cross_compilers, True))
         else:
             ctypes.append((self.build.cross_compilers, True))
         for (complist, is_cross) in ctypes:
@@ -1396,13 +1391,9 @@ int dummy;
                         or langname == 'cs':
                     continue
                 crstr = ''
-                cross_args = []
+                cross_args = self.environment.properties.host.get_external_link_args(langname)
                 if is_cross:
                     crstr = '_CROSS'
-                    try:
-                        cross_args = self.environment.cross_info.config['properties'][langname + '_link_args']
-                    except KeyError:
-                        pass
                 rule = 'rule %s%s_LINKER\n' % (langname, crstr)
                 if compiler.can_linker_accept_rsp():
                     command_template = ''' command = {executable} @$out.rsp
@@ -1667,12 +1658,7 @@ rule FORTRAN_DEP_HACK%s
             self.generate_compile_rule_for(langname, compiler, False, outfile)
             self.generate_pch_rule_for(langname, compiler, False, outfile)
         if self.environment.is_cross_build():
-            # In case we are going a target-only build, make the native compilers
-            # masquerade as cross compilers.
-            if self.environment.cross_info.need_cross_compiler():
-                cclist = self.build.cross_compilers
-            else:
-                cclist = self.build.compilers
+            cclist = self.build.cross_compilers
             for langname, compiler in cclist.items():
                 if compiler.get_id() == 'clang':
                     self.generate_llvm_ir_compile_rule(compiler, True, outfile)
@@ -1835,7 +1821,7 @@ rule FORTRAN_DEP_HACK%s
     def get_cross_stdlib_args(self, target, compiler):
         if not target.is_cross:
             return []
-        if not self.environment.cross_info.has_stdlib(compiler.language):
+        if not self.environment.properties.host.has_stdlib(compiler.language):
             return []
         return compiler.get_no_stdinc_args()
 
@@ -2235,14 +2221,14 @@ rule FORTRAN_DEP_HACK%s
         targetdir = self.get_target_private_dir(target)
         symname = os.path.join(targetdir, target_name + '.symbols')
         elem = NinjaBuildElement(self.all_outputs, symname, 'SHSYM', target_file)
-        if self.environment.is_cross_build() and self.environment.cross_info.need_cross_compiler():
-            elem.add_item('CROSS', '--cross-host=' + self.environment.cross_info.config['host_machine']['system'])
+        if self.environment.is_cross_build():
+            elem.add_item('CROSS', '--cross-host=' + self.environment.machines.host.system)
         elem.write(outfile)
 
     def get_cross_stdlib_link_args(self, target, linker):
         if isinstance(target, build.StaticLibrary) or not target.is_cross:
             return []
-        if not self.environment.cross_info.has_stdlib(linker.language):
+        if not self.environment.properties.host.has_stdlib(linker.language):
             return []
         return linker.get_no_stdlib_link_args()
 
diff --git a/mesonbuild/compilers/c.py b/mesonbuild/compilers/c.py
index 46f41819d..92a9fa6d7 100644
--- a/mesonbuild/compilers/c.py
+++ b/mesonbuild/compilers/c.py
@@ -757,7 +757,7 @@ class CCompiler(Compiler):
         varname = 'has function ' + funcname
         varname = varname.replace(' ', '_')
         if self.is_cross:
-            val = env.cross_info.config['properties'].get(varname, None)
+            val = env.properties.host.get(varname, None)
             if val is not None:
                 if isinstance(val, bool):
                     return val
diff --git a/mesonbuild/compilers/compilers.py b/mesonbuild/compilers/compilers.py
index 31047b14e..2be6ef124 100644
--- a/mesonbuild/compilers/compilers.py
+++ b/mesonbuild/compilers/compilers.py
@@ -1045,13 +1045,10 @@ class Compiler:
     def get_cross_extra_flags(self, environment, link):
         extra_flags = []
         if self.is_cross and environment:
-            if 'properties' in environment.cross_info.config:
-                props = environment.cross_info.config['properties']
-                lang_args_key = self.language + '_args'
-                extra_flags += mesonlib.stringlistify(props.get(lang_args_key, []))
-                lang_link_args_key = self.language + '_link_args'
-                if link:
-                    extra_flags += mesonlib.stringlistify(props.get(lang_link_args_key, []))
+            props = environment.properties.host
+            extra_flags += props.get_external_args(self.language)
+            if link:
+                extra_flags += props.get_external_link_args(self.language)
         return extra_flags
 
     def _get_compile_output(self, dirname, mode):
diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py
index 8ada86a98..b650f78b7 100644
--- a/mesonbuild/coredata.py
+++ b/mesonbuild/coredata.py
@@ -231,42 +231,6 @@ def load_configs(filenames):
     return config
 
 
-def _get_section(config, section):
-    if config.has_section(section):
-        final = {}
-        for k, v in config.items(section):
-            # Windows paths...
-            v = v.replace('\\', '\\\\')
-            try:
-                final[k] = ast.literal_eval(v)
-            except SyntaxError:
-                raise MesonException(
-                    'Malformed value in native file variable: {}'.format(v))
-        return final
-    return {}
-
-
-class ConfigData:
-
-    """Contains configuration information provided by the user for the build."""
-
-    def __init__(self, config=None):
-        if config:
-            self.binaries = _get_section(config, 'binaries')
-            # global is a keyword and globals is a builtin, rather than mangle it,
-            # use a similar word
-            self.universal = _get_section(config, 'globals')
-            self.subprojects = {s: _get_section(config, s) for s in config.sections()
-                                if s not in {'binaries', 'globals'}}
-        else:
-            self.binaries = {}
-            self.universal = {}
-            self.subprojects = {}
-
-    def get_binaries(self, name):
-        return self.binaries.get(name, None)
-
-
 # This class contains all data that must persist over multiple
 # invocations of Meson. It is roughly the same thing as
 # cmakecache.
diff --git a/mesonbuild/dependencies/base.py b/mesonbuild/dependencies/base.py
index cd0293920..35237d09d 100644
--- a/mesonbuild/dependencies/base.py
+++ b/mesonbuild/dependencies/base.py
@@ -33,7 +33,8 @@ from pathlib import PurePath
 from .. import mlog
 from .. import mesonlib
 from ..compilers import clib_langs
-from ..mesonlib import MesonException, OrderedSet
+from ..environment import BinaryTable
+from ..mesonlib import MachineChoice, MesonException, OrderedSet, PerMachine
 from ..mesonlib import Popen_safe, version_compare_many, version_compare, listify
 
 # These must be defined in this file to avoid cyclical references.
@@ -393,24 +394,21 @@ class ConfigToolDependency(ExternalDependency):
         if not isinstance(versions, list) and versions is not None:
             versions = listify(versions)
 
-        if self.env.is_cross_build() and not self.native:
-            cross_file = self.env.cross_info.config['binaries']
-            try:
-                tools = [cross_file[self.tool_name]]
-            except KeyError:
+        for_machine = MachineChoice.BUILD if self.native else MachineChoice.HOST
+        tool = self.env.binaries[for_machine].lookup_entry(self.tool_name)
+        if tool is not None:
+            tools = [tool]
+        else:
+            if self.env.is_cross_build() and not self.native:
                 mlog.warning('No entry for {0} specified in your cross file. '
                              'Falling back to searching PATH. This may find a '
                              'native version of {0}!'.format(self.tool_name))
-                tools = self.tools
-        elif self.tool_name in self.env.config_info.binaries:
-            tools = [self.env.config_info.binaries[self.tool_name]]
-        else:
-            tools = self.tools
+            tools = [[t] for t in self.tools]
 
         best_match = (None, None)
         for tool in tools:
             try:
-                p, out = Popen_safe([tool, '--version'])[:2]
+                p, out = Popen_safe(tool + ['--version'])[:2]
             except (FileNotFoundError, PermissionError):
                 continue
             if p.returncode != 0:
@@ -447,12 +445,12 @@ class ConfigToolDependency(ExternalDependency):
                 mlog.log('Found', mlog.bold(self.tool_name), repr(req_version),
                          mlog.red('NO'))
             return False
-        mlog.log('Found {}:'.format(self.tool_name), mlog.bold(shutil.which(self.config)),
+        mlog.log('Found {}:'.format(self.tool_name), mlog.bold(shutil.which(self.config[0])),
                  '({})'.format(version))
         return True
 
     def get_config_value(self, args, stage):
-        p, out, err = Popen_safe([self.config] + args)
+        p, out, err = Popen_safe(self.config + args)
         # This is required to keep shlex from stripping path separators on
         # Windows. Also, don't put escape sequences in config values, okay?
         out = out.replace('\\', '\\\\')
@@ -469,7 +467,7 @@ class ConfigToolDependency(ExternalDependency):
         return [DependencyMethods.AUTO, DependencyMethods.CONFIG_TOOL]
 
     def get_configtool_variable(self, variable_name):
-        p, out, _ = Popen_safe([self.config, '--{}'.format(variable_name)])
+        p, out, _ = Popen_safe(self.config + ['--{}'.format(variable_name)])
         if p.returncode != 0:
             if self.required:
                 raise DependencyException(
@@ -486,7 +484,7 @@ class ConfigToolDependency(ExternalDependency):
 class PkgConfigDependency(ExternalDependency):
     # The class's copy of the pkg-config path. Avoids having to search for it
     # multiple times in the same Meson invocation.
-    class_pkgbin = None
+    class_pkgbin = PerMachine(None, None, None)
     # We cache all pkg-config subprocess invocations to avoid redundant calls
     pkgbin_cache = {}
 
@@ -498,31 +496,56 @@ class PkgConfigDependency(ExternalDependency):
         # stored in the pickled coredata and recovered.
         self.pkgbin = None
 
-        # When finding dependencies for cross-compiling, we don't care about
-        # the 'native' pkg-config
-        if self.want_cross:
-            if 'pkgconfig' not in environment.cross_info.config['binaries']:
-                if self.required:
-                    raise DependencyException('Pkg-config binary missing from cross file')
+        if not self.want_cross and environment.is_cross_build():
+            for_machine = MachineChoice.BUILD
+        else:
+            for_machine = MachineChoice.HOST
+
+        # Create a nested function for sake of early return
+        def search():
+            # Only search for the pkg-config for each machine the first time and
+            # store the result in the class definition
+            if PkgConfigDependency.class_pkgbin[for_machine] is None:
+                mlog.debug('Pkg-config binary for %s is not cached.' % for_machine)
+            else:
+                mlog.debug('Pkg-config binary for %s is cached.' % for_machine)
+                choice = PkgConfigDependency.class_pkgbin[for_machine]
+                assert choice is not None
+                return choice
+            # Lookup in cross or machine file.
+            bt = environment.binaries[for_machine]
+            potential_pkgpath = bt.lookup_entry('pkgconfig')
+            if potential_pkgpath is None:
+                mlog.debug('Pkg-config binary missing from cross or native file, or PKG_CONFIG undefined.')
             else:
-                potential_pkgbin = ExternalProgram.from_bin_list(
-                    environment.cross_info.config['binaries'], 'pkgconfig')
-                if potential_pkgbin.found():
-                    self.pkgbin = potential_pkgbin
+                mlog.debug('Pkg-config binary for %s specified from config file as %s.', for_machine, potential_pkgpath)
+                potential_pkgbin = ExternalProgram.from_entry('pkgconfig', potential_pkgpath)
+                if not potential_pkgbin.found():
+                    mlog.debug(
+                        'Pkg-config %s for machine %s specified at %s but not found.',
+                        potential_pkgbin.name, for_machine, potential_pkgbin.command)
                 else:
-                    mlog.debug('Cross pkg-config %s not found.' % potential_pkgbin.name)
-        # Only search for the native pkg-config the first time and
-        # store the result in the class definition
-        elif PkgConfigDependency.class_pkgbin is None:
-            self.pkgbin = self.check_pkgconfig()
-            PkgConfigDependency.class_pkgbin = self.pkgbin
-        else:
-            self.pkgbin = PkgConfigDependency.class_pkgbin
+                    return potential_pkgbin
+            # Fallback on hard-coded defaults.
+            if environment.machines.matches_build_machine(for_machine):
+                for potential_pkgpath in environment.default_pkgconfig:
+                    potential_pkgbin = self.check_pkgconfig(potential_pkgpath)
+                    if potential_pkgbin is None:
+                        mlog.debug(
+                            'default Pkg-config fallback %s for machine %s specified at %s but not found.',
+                            potential_pkgbin.name, for_machine, potential_pkgbin.command)
+                    else:
+                        return potential_pkgbin
 
-        if not self.pkgbin:
+        self.pkgbin = search()
+        if self.pkgbin is None:
+            msg = 'Pkg-config binary for machine %s not found.' % for_machine
             if self.required:
-                raise DependencyException('Pkg-config not found.')
-            return
+                raise DependencyException(msg)
+            else:
+                mlog.debug(msg)
+        else:
+            PkgConfigDependency.class_pkgbin[for_machine] = self.pkgbin
 
         mlog.debug('Determining dependency {!r} with pkg-config executable '
                    '{!r}'.format(name, self.pkgbin.get_path()))
@@ -785,12 +808,7 @@ class PkgConfigDependency(ExternalDependency):
     def get_methods():
         return [DependencyMethods.PKGCONFIG]
 
-    def check_pkgconfig(self):
-        evar = 'PKG_CONFIG'
-        if evar in os.environ:
-            pkgbin = os.environ[evar].strip()
-        else:
-            pkgbin = 'pkg-config'
+    def check_pkgconfig(self, pkgbin):
         pkgbin = ExternalProgram(pkgbin, silent=True)
         if pkgbin.found():
             try:
@@ -1688,14 +1706,15 @@ class ExternalProgram:
         '''Human friendly description of the command'''
         return ' '.join(self.command)
 
-    @staticmethod
-    def from_bin_list(bins, name):
-        if name not in bins:
+    @classmethod
+    def from_bin_list(cls, bt: BinaryTable, name):
+        command = bt.lookup_entry(name)
+        if command is None:
             return NonExistingExternalProgram()
-        command = bins[name]
-        if not isinstance(command, (list, str)):
-            raise MesonException('Invalid type {!r} for binary {!r} in cross file'
-                                 ''.format(command, name))
+        return cls.from_entry(name, command)
+
+    @staticmethod
+    def from_entry(name, command):
         if isinstance(command, list):
             if len(command) == 1:
                 command = command[0]
@@ -1703,6 +1722,7 @@ class ExternalProgram:
         # need to search if the path is an absolute path.
         if isinstance(command, list) or os.path.isabs(command):
             return ExternalProgram(name, command=command, silent=True)
+        assert isinstance(command, str)
         # Search for the command using the specified string!
         return ExternalProgram(command, silent=True)
 
diff --git a/mesonbuild/dependencies/ui.py b/mesonbuild/dependencies/ui.py
index 7b3d051e9..03c96f223 100644
--- a/mesonbuild/dependencies/ui.py
+++ b/mesonbuild/dependencies/ui.py
@@ -333,22 +333,18 @@ class QtBaseDependency(ExternalDependency):
             if prefix:
                 self.bindir = os.path.join(prefix, 'bin')
 
-    def _find_qmake(self, qmake):
-        # Even when cross-compiling, if a cross-info qmake is not specified, we
-        # fallback to using the qmake in PATH because that's what we used to do
-        if self.env.is_cross_build():
-            if 'qmake' in self.env.cross_info.config['binaries']:
-                return ExternalProgram.from_bin_list(self.env.cross_info.config['binaries'], 'qmake')
-        elif self.env.config_info:
-            # Prefer suffixed to unsuffixed version
-            p = ExternalProgram.from_bin_list(self.env.config_info.binaries, 'qmake')
-            if p.found():
-                return p
-        return ExternalProgram(qmake, silent=True)
-
     def _qmake_detect(self, mods, kwargs):
         for qmake in ('qmake-' + self.name, 'qmake'):
-            self.qmake = self._find_qmake(qmake)
+            self.qmake = ExternalProgram.from_bin_list(
+                self.env.binaries.host, qmake)
+            if not self.qmake.found():
+                # Even when cross-compiling, if a cross-info qmake is not
+                # specified, we fallback to using the qmake in PATH because
+                # that's what we used to do
+                self.qmake = ExternalProgram.from_bin_list(
+                    self.env.binaries.build, qmake)
+            if not self.qmake.found():
+                self.qmake = ExternalProgram(qmake, silent=True)
             if not self.qmake.found():
                 continue
             # Check that the qmake is for qt5
diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py
index 71f75f906..b10f826ee 100644
--- a/mesonbuild/environment.py
+++ b/mesonbuild/environment.py
@@ -17,7 +17,9 @@ import configparser, os, platform, re, sys, shlex, shutil, subprocess
 from . import coredata
 from .linkers import ArLinker, ArmarLinker, VisualStudioLinker, DLinker, CcrxLinker
 from . import mesonlib
-from .mesonlib import MesonException, EnvironmentException, PerMachine, Popen_safe
+from .mesonlib import (
+    MesonException, EnvironmentException, MachineChoice, PerMachine, Popen_safe
+)
 from . import mlog
 
 from . import compilers
@@ -336,32 +338,46 @@ class Environment:
         else:
             # Just create a fresh coredata in this case
             self.create_new_coredata(options)
-        self.exe_wrapper = None
 
         self.machines = MachineInfos()
         # Will be fully initialized later using compilers later.
         self.machines.detect_build()
-        if self.coredata.cross_file:
-            self.cross_info = CrossBuildInfo(self.coredata.cross_file)
-            if 'exe_wrapper' in self.cross_info.config['binaries']:
-                from .dependencies import ExternalProgram
-                self.exe_wrapper = ExternalProgram.from_bin_list(
-                    self.cross_info.config['binaries'], 'exe_wrapper')
-            if 'host_machine' in self.cross_info.config:
-                self.machines.host = MachineInfo.from_literal(
-                    self.cross_info.config['host_machine'])
-            if 'target_machine' in self.cross_info.config:
-                self.machines.target = MachineInfo.from_literal(
-                    self.cross_info.config['target_machine'])
-        else:
-            self.cross_info = None
-        self.machines.default_missing()
 
-        if self.coredata.config_files:
-            self.config_info = coredata.ConfigData(
+        # Similar to coredata.compilers and build.compilers, but lower level in
+        # that there is no meta data, only names/paths.
+        self.binaries = PerMachineDefaultable()
+        # Just uses hard-coded defaults and environment variables. Might be
+        # overwritten by a native file.
+        self.binaries.build = BinaryTable({})
+
+        # Misc other properties about each machine.
+        self.properties = PerMachine(Properties(), Properties(), Properties())
+
+        if self.coredata.config_files is not None:
+            config = MesonConfigFile.from_config_parser(
                 coredata.load_configs(self.coredata.config_files))
+            self.binaries.build = BinaryTable(config.get('binaries', {}))
+
+        if self.coredata.cross_file is not None:
+            config = MesonConfigFile.parse_datafile(self.coredata.cross_file)
+            self.properties.host.properties = config.get('properties', {})
+            self.binaries.host = BinaryTable(config.get('binaries', {}), False)
+            if 'host_machine' in config:
+                self.machines.host = MachineInfo.from_literal(config['host_machine'])
+            if 'target_machine' in config:
+                self.machines.target = MachineInfo.from_literal(config['target_machine'])
+
+        self.machines.default_missing()
+        self.binaries.default_missing()
+
+        exe_wrapper = self.binaries.host.lookup_entry('exe_wrapper')
+        if exe_wrapper is not None:
+            from .dependencies import ExternalProgram
+            self.exe_wrapper = ExternalProgram.from_bin_list(
+                self.binaries.host,
+                'exe_wrapper')
         else:
-            self.config_info = coredata.ConfigData()
+            self.exe_wrapper = None
 
         self.cmd_line_options = options.cmd_line_options.copy()
 
@@ -385,10 +401,12 @@ class Environment:
         self.default_swift = ['swiftc']
         self.default_vala = ['valac']
         self.default_static_linker = ['ar']
+        self.default_strip = ['strip']
         self.vs_static_linker = ['lib']
         self.clang_cl_static_linker = ['llvm-lib']
         self.gcc_static_linker = ['gcc-ar']
         self.clang_static_linker = ['llvm-ar']
+        self.default_pkgconfig = ['pkg-config']
 
         # Various prefixes and suffixes for import libraries, shared libraries,
         # static libraries, and executables.
@@ -406,11 +424,6 @@ class Environment:
             self.exe_suffix = ''
             self.object_suffix = 'o'
             self.win_libdir_layout = False
-        if 'STRIP' in os.environ:
-            self.native_strip_bin = shlex.split(
-                os.environ[BinaryTable.evarMap['strip']])
-        else:
-            self.native_strip_bin = ['strip']
 
     def create_new_coredata(self, options):
         # WARNING: Don't use any values from coredata in __init__. It gets
@@ -422,7 +435,7 @@ class Environment:
         self.first_invocation = True
 
     def is_cross_build(self):
-        return self.cross_info is not None
+        return not self.machines.matches_build_machine(MachineChoice.HOST)
 
     def dump_coredata(self):
         return coredata.save(self.coredata, self.get_build_dir())
@@ -521,32 +534,30 @@ class Environment:
         The list of compilers is detected in the exact same way for
         C, C++, ObjC, ObjC++, Fortran, CS so consolidate it here.
         '''
-        is_cross = False
-        exe_wrap = None
-        evar = BinaryTable.evarMap[lang]
-
-        if self.is_cross_build() and want_cross:
-            if lang not in self.cross_info.config['binaries']:
-                raise EnvironmentException('{!r} compiler binary not defined in cross file'.format(lang))
-            compilers, ccache = BinaryTable.parse_entry(
-                mesonlib.stringlistify(self.cross_info.config['binaries'][lang]))
-            BinaryTable.warn_about_lang_pointing_to_cross(compilers[0], evar)
-            # Return value has to be a list of compiler 'choices'
-            compilers = [compilers]
-            is_cross = True
-            exe_wrap = self.get_exe_wrapper()
-        elif evar in os.environ:
-            compilers, ccache = BinaryTable.parse_entry(
-                shlex.split(os.environ[evar]))
+
+        # This morally assumes `want_cross = !native`. It may not yet be
+        # consistently set that way in the non cross build case, but it doesn't
+        # really matter since both options are the same in that case.
+        for_machine = MachineChoice.HOST if want_cross else MachineChoice.BUILD
+
+        value = self.binaries[for_machine].lookup_entry(lang)
+        if value is not None:
+            compilers, ccache = BinaryTable.parse_entry(value)
             # Return value has to be a list of compiler 'choices'
             compilers = [compilers]
-        elif lang in self.config_info.binaries:
-            compilers, ccache = BinaryTable.parse_entry(
-                mesonlib.stringlistify(self.config_info.binaries[lang]))
-            compilers = [compilers]
         else:
+            if not self.machines.matches_build_machine(for_machine):
+                raise EnvironmentException('{!r} compiler binary not defined in cross or native file'.format(lang))
             compilers = getattr(self, 'default_' + lang)
             ccache = BinaryTable.detect_ccache()
+
+        if self.machines.matches_build_machine(for_machine):
+            is_cross = False
+            exe_wrap = None
+        else:
+            is_cross = True
+            exe_wrap = self.get_exe_wrapper()
+
         return compilers, ccache, is_cross, exe_wrap
 
     def _handle_exceptions(self, exceptions, binaries, bintype='compiler'):
@@ -826,9 +837,8 @@ class Environment:
         self._handle_exceptions(popen_exceptions, compilers)
 
     def detect_java_compiler(self):
-        if 'java' in self.config_info.binaries:
-            exelist = mesonlib.stringlistify(self.config_info.binaries['java'])
-        else:
+        exelist = self.binaries.host.lookup_entry('java')
+        if exelist is None:
             # TODO support fallback
             exelist = [self.default_java[0]]
 
@@ -862,13 +872,11 @@ class Environment:
         self._handle_exceptions(popen_exceptions, compilers)
 
     def detect_vala_compiler(self):
-        if 'VALAC' in os.environ:
-            exelist = shlex.split(os.environ['VALAC'])
-        elif 'vala' in self.config_info.binaries:
-            exelist = mesonlib.stringlistify(self.config_info.binaries['vala'])
-        else:
+        exelist = self.binaries.host.lookup_entry('vala')
+        if exelist is None:
             # TODO support fallback
             exelist = [self.default_vala[0]]
+
         try:
             p, out = Popen_safe(exelist + ['--version'])[0:2]
         except OSError:
@@ -899,20 +907,15 @@ class Environment:
         self._handle_exceptions(popen_exceptions, compilers)
 
     def detect_d_compiler(self, want_cross):
-        is_cross = False
+        is_cross = want_cross and self.is_cross_build()
+        exelist = self.binaries.host.lookup_entry('d')
         # Search for a D compiler.
         # We prefer LDC over GDC unless overridden with the DC
         # environment variable because LDC has a much more
         # up to date language version at time (2016).
-        if 'DC' in os.environ:
-            exelist = shlex.split(os.environ['DC'])
+        if exelist is not None:
             if os.path.basename(exelist[-1]).startswith(('ldmd', 'gdmd')):
                     raise EnvironmentException('Meson doesn\'t support %s as it\'s only a DMD frontend for another compiler. Please provide a valid value for DC or unset it so that Meson can resolve the compiler by itself.' % exelist[-1])
-        elif self.is_cross_build() and want_cross:
-            exelist = mesonlib.stringlistify(self.cross_info.config['binaries']['d'])
-            is_cross = True
-        elif 'd' in self.config_info.binaries:
-            exelist = mesonlib.stringlistify(self.config_info.binaries['d'])
         else:
             for d in self.default_d:
                 if shutil.which(d):
@@ -947,11 +950,11 @@ class Environment:
         raise EnvironmentException('Unknown compiler "' + ' '.join(exelist) + '"')
 
     def detect_swift_compiler(self):
-        if 'swift' in self.config_info.binaries:
-            exelist = mesonlib.stringlistify(self.config_info.binaries['swift'])
-        else:
+        exelist = self.binaries.host.lookup_entry('swift')
+        if exelist is None:
             # TODO support fallback
             exelist = [self.default_swift[0]]
+
         try:
             p, _, err = Popen_safe(exelist + ['-v'])
         except OSError:
@@ -1013,16 +1016,11 @@ class Environment:
         return comp, cross_comp
 
     def detect_static_linker(self, compiler):
-        if compiler.is_cross:
-            linker = self.cross_info.config['binaries']['ar']
-            if isinstance(linker, str):
-                linker = [linker]
+        linker = self.binaries.host.lookup_entry('ar')
+        if linker is not None:
             linkers = [linker]
         else:
-            evar = BinaryTable.evarMap['ar']
-            if evar in os.environ:
-                linkers = [shlex.split(os.environ[evar])]
-            elif isinstance(compiler, compilers.VisualStudioCCompiler):
+            if isinstance(compiler, compilers.VisualStudioCCompiler):
                 linkers = [self.vs_static_linker, self.clang_cl_static_linker]
             elif isinstance(compiler, compilers.GnuCompiler):
                 # Use gcc-ar if available; needed for LTO
@@ -1140,37 +1138,39 @@ class Environment:
         out = out.split('\n')[index].lstrip('libraries: =').split(':')
         return [os.path.normpath(p) for p in out]
 
+    def need_exe_wrapper(self, for_machine: MachineChoice = MachineChoice.HOST):
+        value = self.properties[for_machine].get('needs_exe_wrapper', None)
+        if value is not None:
+            return value
+        return not self.machines[for_machine].can_run()
+
     def get_exe_wrapper(self):
-        if not self.cross_info.need_exe_wrapper():
+        if not self.need_exe_wrapper():
             from .dependencies import EmptyExternalProgram
             return EmptyExternalProgram()
         return self.exe_wrapper
 
-
-class CrossBuildInfo:
-    def __init__(self, filename):
-        self.config = {'properties': {}}
-        self.parse_datafile(filename)
-        if 'host_machine' not in self.config and 'target_machine' not in self.config:
-            raise mesonlib.MesonException('Cross info file must have either host or a target machine.')
-        if 'host_machine' in self.config and 'binaries' not in self.config:
-            raise mesonlib.MesonException('Cross file with "host_machine" is missing "binaries".')
-
-    def ok_type(self, i):
-        return isinstance(i, (str, int, bool))
-
-    def parse_datafile(self, filename):
+class MesonConfigFile:
+    @classmethod
+    def parse_datafile(cls, filename):
         config = configparser.ConfigParser()
         try:
             with open(filename, 'r') as f:
                 config.read_file(f, filename)
         except FileNotFoundError:
             raise EnvironmentException('File not found: %s.' % filename)
+        return cls.from_config_parser(config)
+
+    @classmethod
+    def from_config_parser(cls, parser: configparser.ConfigParser):
+        out = {}
         # This is a bit hackish at the moment.
-        for s in config.sections():
-            self.config[s] = {}
-            for entry in config[s]:
-                value = config[s][entry]
+        for s in parser.sections():
+            section = {}
+            for entry in parser[s]:
+                value = parser[s][entry]
+                # Windows paths...
+                value = value.replace('\\', '\\\\')
                 if ' ' in entry or '\t' in entry or "'" in entry or '"' in entry:
                     raise EnvironmentException('Malformed variable name %s in cross file..' % entry)
                 try:
@@ -1178,61 +1178,48 @@ class CrossBuildInfo:
                 except Exception:
                     raise EnvironmentException('Malformed value in cross file variable %s.' % entry)
 
-                if self.ok_type(res):
-                    self.config[s][entry] = res
-                elif isinstance(res, list):
-                    for i in res:
-                        if not self.ok_type(i):
-                            raise EnvironmentException('Malformed value in cross file variable %s.' % entry)
-                    self.config[s][entry] = res
-                else:
-                    raise EnvironmentException('Malformed value in cross file variable %s.' % entry)
+                for i in (res if isinstance(res, list) else [res]):
+                    if not isinstance(i, (str, int, bool)):
+                        raise EnvironmentException('Malformed value in cross file variable %s.' % entry)
 
-    def has_host(self):
-        return 'host_machine' in self.config
+                section[entry] = res
 
-    def has_target(self):
-        return 'target_machine' in self.config
+            out[s] = section
+        return out
 
-    def has_stdlib(self, language):
-        return language + '_stdlib' in self.config['properties']
+class Properties:
+    def __init__(self):
+        self.properties = {}
 
-    def get_stdlib(self, language):
-        return self.config['properties'][language + '_stdlib']
+    def get_external_args(self, language):
+        return mesonlib.stringlistify(self.properties.get(language + '_args', []))
+
+    def get_external_link_args(self, language):
+        return mesonlib.stringlistify(self.properties.get(language + '_link_args', []))
 
-    def get_host_system(self):
-        "Name of host system like 'linux', or None"
-        if self.has_host():
-            return self.config['host_machine']['system']
-        return None
+    def has_stdlib(self, language):
+        return language + '_stdlib' in self.properties
 
-    def get_properties(self):
-        return self.config['properties']
+    def get_stdlib(self, language):
+        return self.properties[language + '_stdlib']
 
     def get_root(self):
-        return self.get_properties().get('root', None)
+        return self.properties.get('root', None)
 
     def get_sys_root(self):
-        return self.get_properties().get('sys_root', None)
+        return self.properties.get('sys_root', None)
 
-    # When compiling a cross compiler we use the native compiler for everything.
-    # But not when cross compiling a cross compiler.
-    def need_cross_compiler(self):
-        return 'host_machine' in self.config
+    # TODO consider removing so Properties is less freeform
+    def __getitem__(self, key):
+        return self.properties[key]
 
-    def need_exe_wrapper(self):
-        value = self.config['properties'].get('needs_exe_wrapper', None)
-        if value is not None:
-            return value
-        # Can almost always run 32-bit binaries on 64-bit natively if the host
-        # and build systems are the same. We don't pass any compilers to
-        # detect_cpu_family() here because we always want to know the OS
-        # architecture, not what the compiler environment tells us.
-        if self.has_host() and detect_cpu_family({}) == 'x86_64' and \
-           self.config['host_machine']['cpu_family'] == 'x86' and \
-           self.config['host_machine']['system'] == detect_system():
-            return False
-        return True
+    # TODO consider removing so Properties is less freeform
+    def __contains__(self, item):
+        return item in self.properties
+
+    # TODO consider removing, for same reasons as above
+    def get(self, key, default=None):
+        return self.properties.get(key, default)
 
 class MachineInfo:
     def __init__(self, system, cpu_family, cpu, endian):
@@ -1354,7 +1341,26 @@ class MachineInfo:
         return self.is_windows() \
             or self.is_cygwin()
 
-class MachineInfos(PerMachine):
+    # TODO make this compare two `MachineInfo`s purely. How important is the
+    # `detect_cpu_family({})` distinction? It is the one impediment to that.
+    def can_run(self):
+        """Whether we can run binaries for this machine on the current machine.
+
+        Can almost always run 32-bit binaries on 64-bit natively if the host
+        and build systems are the same. We don't pass any compilers to
+        detect_cpu_family() here because we always want to know the OS
+        architecture, not what the compiler environment tells us.
+        """
+        if self.system != detect_system():
+            return False
+        true_build_cpu_family = detect_cpu_family({})
+        return \
+            (self.cpu_family == true_build_cpu_family) or \
+            ((true_build_cpu_family == 'x86_64') and (self.cpu_family == 'x86'))
+
+class PerMachineDefaultable(PerMachine):
+    """Extends `PerMachine` with the ability to default from `None`s.
+    """
     def __init__(self):
         super().__init__(None, None, None)
 
@@ -1382,10 +1388,24 @@ class MachineInfos(PerMachine):
         if self.host == self.build:
             self.host = None
 
+class MachineInfos(PerMachineDefaultable):
     def detect_build(self, compilers = None):
         self.build = MachineInfo.detect(compilers)
 
+    def matches_build_machine(self, machine: MachineChoice):
+        return self.build == self[machine]
+
 class BinaryTable:
+    def __init__(self, binaries = {}, fallback = True):
+        self.binaries = binaries
+        self.fallback = fallback
+        for name, command in self.binaries.items():
+            if not isinstance(command, (list, str)):
+                # TODO generalize message
+                raise mesonlib.MesonException(
+                    'Invalid type {!r} for binary {!r} in cross file'
+                    ''.format(command, name))
+
     # Map from language identifiers to environment variables.
     evarMap = {
         # Compilers
@@ -1402,6 +1422,9 @@ class BinaryTable:
         # Binutils
         'strip': 'STRIP',
         'ar': 'AR',
+        'windres': 'WINDRES',
+
+        'pkgconfig': 'PKG_CONFIG',
     }
 
     @classmethod
@@ -1417,7 +1440,7 @@ class BinaryTable:
         return cmdlist
 
     @classmethod
-    def warn_about_lang_pointing_to_cross(cls, compiler_exe, evar):
+    def _warn_about_lang_pointing_to_cross(cls, compiler_exe, evar):
         evar_str = os.environ.get(evar, 'WHO_WOULD_CALL_THEIR_COMPILER_WITH_THIS_NAME')
         if evar_str == compiler_exe:
             mlog.warning('''Env var %s seems to point to the cross compiler.
@@ -1434,3 +1457,26 @@ This is probably wrong, it should always point to the native compiler.''' % evar
             ccache = []
         # Return value has to be a list of compiler 'choices'
         return compiler, ccache
+
+    def lookup_entry(self, name):
+        """Lookup binary
+
+        Returns command with args as list if found, Returns `None` if nothing is
+        found.
+
+        First tries looking in explicit map, then tries environment variable.
+        """
+        # Try explict map, don't fall back on env var
+        command = self.binaries.get(name)
+        if command is not None:
+            command = mesonlib.stringlistify(command)
+            # Relies on there being no "" env var
+            evar = self.evarMap.get(name, "")
+            self._warn_about_lang_pointing_to_cross(command[0], evar)
+        elif self.fallback:
+            # Relies on there being no "" env var
+            evar = self.evarMap.get(name, "")
+            command = os.environ.get(evar)
+            if command is not None:
+                command = shlex.split(command)
+        return command
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
index 61481a491..f849f3ce0 100644
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -21,7 +21,7 @@ from . import optinterpreter
 from . import compilers
 from .wrap import wrap, WrapMode
 from . import mesonlib
-from .mesonlib import FileMode, Popen_safe, listify, extract_as_list, has_path_sep
+from .mesonlib import FileMode, MachineChoice, Popen_safe, listify, extract_as_list, has_path_sep
 from .dependencies import ExternalProgram
 from .dependencies import InternalDependency, Dependency, NotFoundDependency, DependencyException
 from .interpreterbase import InterpreterBase
@@ -1760,7 +1760,7 @@ class MesonMain(InterpreterObject):
     @permittedKwargs({})
     def has_exe_wrapper_method(self, args, kwargs):
         if self.is_cross_build_method(None, None) and \
-           self.build.environment.cross_info.need_exe_wrapper():
+           self.build.environment.need_exe_wrapper():
             if self.build.environment.exe_wrapper is None:
                 return False
         # We return True when exe_wrap is defined, when it's not needed, and
@@ -1866,7 +1866,7 @@ class MesonMain(InterpreterObject):
         if not isinstance(propname, str):
             raise InterpreterException('Property name must be string.')
         try:
-            props = self.interpreter.environment.cross_info.get_properties()
+            props = self.interpreter.environment.properties.host
             return props[propname]
         except Exception:
             if len(args) == 2:
@@ -2164,10 +2164,10 @@ class Interpreter(InterpreterBase):
 
     def check_cross_stdlibs(self):
         if self.build.environment.is_cross_build():
-            cross_info = self.build.environment.cross_info
+            props = self.build.environment.properties.host
             for l, c in self.build.cross_compilers.items():
                 try:
-                    di = mesonlib.stringlistify(cross_info.get_stdlib(l))
+                    di = mesonlib.stringlistify(props.get_stdlib(l))
                     if len(di) != 2:
                         raise InterpreterException('Stdlib definition for %s should have exactly two elements.'
                                                    % l)
@@ -2685,7 +2685,7 @@ external dependencies (including libraries) must go to "dependencies".''')
 
     def add_languages(self, args, required):
         success = True
-        need_cross_compiler = self.environment.is_cross_build() and self.environment.cross_info.need_cross_compiler()
+        need_cross_compiler = self.environment.is_cross_build()
         for lang in sorted(args, key=compilers.sort_clink):
             lang = lang.lower()
             if lang in self.coredata.compilers:
@@ -2735,7 +2735,8 @@ external dependencies (including libraries) must go to "dependencies".''')
             self.coredata. base_options[optname] = oobj
         self.emit_base_options_warnings(enabled_opts)
 
-    def _program_from_file(self, prognames, bins, silent):
+    def program_from_file_for(self, for_machine, prognames, silent):
+        bins = self.environment.binaries[for_machine]
         for p in prognames:
             if hasattr(p, 'held_object'):
                 p = p.held_object
@@ -2748,14 +2749,6 @@ external dependencies (including libraries) must go to "dependencies".''')
                 return ExternalProgramHolder(prog)
         return None
 
-    def program_from_cross_file(self, prognames, silent=False):
-        bins = self.environment.cross_info.config['binaries']
-        return self._program_from_file(prognames, bins, silent)
-
-    def program_from_config_file(self, prognames, silent=False):
-        bins = self.environment.config_info.binaries
-        return self._program_from_file(prognames, bins, silent)
-
     def program_from_system(self, args, silent=False):
         # Search for scripts relative to current subdir.
         # Do not cache found programs because find_program('foobar')
@@ -2813,11 +2806,8 @@ external dependencies (including libraries) must go to "dependencies".''')
 
         progobj = self.program_from_overrides(args, silent=silent)
         if progobj is None:
-            if self.build.environment.is_cross_build() and not native:
-                progobj = self.program_from_cross_file(args, silent=silent)
-            else:
-                progobj = self.program_from_config_file(args, silent=silent)
-
+            for_machine = MachineChoice.BUILD if native else MachineChoice.HOST
+            progobj = self.program_from_file_for(for_machine, args, silent=silent)
         if progobj is None:
             progobj = self.program_from_system(args, silent=silent)
         if required and (progobj is None or not progobj.found()):
@@ -4121,8 +4111,9 @@ This will become a hard error in the future.''')
 
     def add_cross_stdlib_info(self, target):
         for l in self.get_used_languages(target):
-            if self.environment.cross_info.has_stdlib(l) \
-                    and self.subproject != self.environment.cross_info.get_stdlib(l)[0]:
+            props = self.environment.properties.host
+            if props.has_stdlib(l) \
+                    and self.subproject != props.get_stdlib(l)[0]:
                 target.add_deps(self.build.cross_stdlibs[l])
 
     def check_sources_exist(self, subdir, sources):
diff --git a/mesonbuild/modules/gnome.py b/mesonbuild/modules/gnome.py
index be9905994..2ab575c4c 100644
--- a/mesonbuild/modules/gnome.py
+++ b/mesonbuild/modules/gnome.py
@@ -532,7 +532,7 @@ class GnomeModule(ExtensionModule):
 
         for lang in langs:
             if state.environment.is_cross_build():
-                link_args = state.environment.cross_info.config["properties"].get(lang + '_link_args', "")
+                link_args = state.environment.properties.host.get_external_link_args(lang)
             else:
                 link_args = state.environment.coredata.get_external_link_args(lang)
 
@@ -715,7 +715,7 @@ class GnomeModule(ExtensionModule):
         ret = []
         for lang in langs:
             if state.environment.is_cross_build():
-                ret += state.environment.cross_info.config["properties"].get(lang + '_args', "")
+                ret += state.environment.properties.host.get_external_args(lang)
             else:
                 ret += state.environment.coredata.get_external_args(lang)
         return ret
@@ -1043,8 +1043,8 @@ This will become a hard error in the future.''')
         ldflags.update(external_ldflags)
 
         if state.environment.is_cross_build():
-            cflags.update(state.environment.cross_info.config["properties"].get('c_args', ""))
-            ldflags.update(state.environment.cross_info.config["properties"].get('c_link_args', ""))
+            cflags.update(state.environment.properties.host.get_external_args('c'))
+            ldflags.update(state.environment.properties.host.get_external_link_args('c'))
             compiler = state.environment.coredata.cross_compilers.get('c')
         else:
             cflags.update(state.environment.coredata.get_external_args('c'))
diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py
index 9643ebc44..c14ac8588 100644
--- a/mesonbuild/modules/python.py
+++ b/mesonbuild/modules/python.py
@@ -501,20 +501,17 @@ class PythonModule(ExtensionModule):
         if len(args) > 1:
             raise InvalidArguments('find_installation takes zero or one positional argument.')
 
-        if 'python' in state.environment.config_info.binaries:
-            name_or_path = state.environment.config_info.binaries['python']
-        elif args:
+        name_or_path = state.environment.binaries.host.lookup_entry('python')
+        if name_or_path is None and args:
             name_or_path = args[0]
             if not isinstance(name_or_path, str):
                 raise InvalidArguments('find_installation argument must be a string.')
-        else:
-            name_or_path = None
 
         if not name_or_path:
             mlog.log("Using meson's python {}".format(mesonlib.python_command))
             python = ExternalProgram('python3', mesonlib.python_command, silent=True)
         else:
-            python = ExternalProgram(name_or_path, silent = True)
+            python = ExternalProgram.from_entry('python3', name_or_path)
 
             if not python.found() and mesonlib.is_windows():
                 pythonpath = self._get_win_pythonpath(name_or_path)
diff --git a/mesonbuild/modules/python3.py b/mesonbuild/modules/python3.py
index f6646327a..46f15f04e 100644
--- a/mesonbuild/modules/python3.py
+++ b/mesonbuild/modules/python3.py
@@ -48,10 +48,11 @@ class Python3Module(ExtensionModule):
 
     @noKwargs
     def find_python(self, state, args, kwargs):
-        options = [state.environment.config_info.binaries.get('python3')]
-        if not options[0]:  # because this would be [None]
-            options = ['python3', mesonlib.python_command]
-        py3 = dependencies.ExternalProgram(*options, silent=True)
+        command = state.environment.binaries.host.lookup_entry('python3')
+        if command is not None:
+            py3 = dependencies.ExternalProgram.from_entry('python3', command)
+        else:
+            py3 = dependencies.ExternalProgram('python3', mesonlib.python_command, silent=True)
         return ModuleReturnValue(py3, [py3])
 
     @noKwargs
diff --git a/mesonbuild/modules/windows.py b/mesonbuild/modules/windows.py
index d185d8913..e8d266e54 100644
--- a/mesonbuild/modules/windows.py
+++ b/mesonbuild/modules/windows.py
@@ -41,29 +41,13 @@ class WindowsModule(ExtensionModule):
     def _find_resource_compiler(self, state):
         # FIXME: Does not handle `native: true` executables, see
         # See https://github.com/mesonbuild/meson/issues/1531
+        # But given a machine, we can un-hardcode `binaries.host` below.
 
         if hasattr(self, '_rescomp'):
             return self._rescomp
 
-        rescomp = None
-        if state.environment.is_cross_build():
-            # If cross compiling see if windres has been specified in the
-            # cross file before trying to find it another way.
-            bins = state.environment.cross_info.config['binaries']
-            rescomp = ExternalProgram.from_bin_list(bins, 'windres')
-
-        if not rescomp or not rescomp.found():
-            if 'WINDRES' in os.environ:
-                # Pick-up env var WINDRES if set. This is often used for
-                # specifying an arch-specific windres.
-                rescomp = ExternalProgram('windres', command=os.environ.get('WINDRES'), silent=True)
-
-        if not rescomp or not rescomp.found():
-            # Take windres from the config file after the environment, which is
-            # in keeping with the expectations on unix-like OSes that
-            # environment variables trump config files.
-            bins = state.environment.config_info.binaries
-            rescomp = ExternalProgram.from_bin_list(bins, 'windres')
+        # Will try cross / native file and then env var
+        rescomp = ExternalProgram.from_bin_list(state.environment.binaries.host, 'windres')
 
         if not rescomp or not rescomp.found():
             comp = self.detect_compiler(state.compilers)


Test patch:
diff --git a/run_tests.py b/run_tests.py
index aa8a5898e..25a2d7f97 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -76,8 +76,10 @@ def get_fake_options(prefix):
     opts.native_file = []
     return opts
 
-def get_fake_env(sdir, bdir, prefix):
-    env = Environment(sdir, bdir, get_fake_options(prefix))
+def get_fake_env(sdir, bdir, prefix, opts = None):
+    if opts is None:
+        opts = get_fake_options(prefix)
+    env = Environment(sdir, bdir, opts)
     env.coredata.compiler_options['c_args'] = FakeCompilerOptions()
     return env
 
diff --git a/run_unittests.py b/run_unittests.py
index 07e6bf9be..46f93e72f 100755
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -54,7 +54,7 @@ import mesonbuild.modules.pkgconfig
 from run_tests import (
     Backend, FakeBuild, FakeCompilerOptions,
     ensure_backend_detects_changes, exe_suffix, get_backend_commands,
-    get_builddir_target_args, get_fake_env, get_meson_script,
+    get_builddir_target_args, get_fake_env, get_fake_options, get_meson_script,
     run_configure_inprocess, run_mtest_inprocess
 )
 
@@ -542,7 +542,10 @@ class InternalTests(unittest.TestCase):
         config.write(configfile)
         configfile.flush()
         configfile.close()
-        detected_value = mesonbuild.environment.CrossBuildInfo(configfile.name).need_exe_wrapper()
+        opts = get_fake_options('')
+        opts.cross_file = configfilename
+        env = get_fake_env('', '', '', opts)
+        detected_value = env.need_exe_wrapper()
         os.unlink(configfilename)
 
         desired_value = not detected_value
@@ -554,7 +557,10 @@ class InternalTests(unittest.TestCase):
         configfilename = configfile.name
         config.write(configfile)
         configfile.close()
-        forced_value = mesonbuild.environment.CrossBuildInfo(configfile.name).need_exe_wrapper()
+        opts = get_fake_options('')
+        opts.cross_file = configfilename
+        env = get_fake_env('', '', '', opts)
+        forced_value = env.need_exe_wrapper()
         os.unlink(configfilename)
 
         self.assertEqual(forced_value, desired_value)
@@ -744,8 +750,9 @@ class InternalTests(unittest.TestCase):
 
             old_call = PkgConfigDependency._call_pkgbin
             old_check = PkgConfigDependency.check_pkgconfig
+            old_pkgbin = PkgConfigDependency.class_pkgbin
             PkgConfigDependency._call_pkgbin = fake_call_pkgbin
-            PkgConfigDependency.check_pkgconfig = lambda x: pkgbin
+            PkgConfigDependency.check_pkgconfig = lambda x, _: pkgbin
             # Test begins
             kwargs = {'required': True, 'silent': True}
             foo_dep = PkgConfigDependency('foo', env, kwargs)
@@ -766,7 +773,7 @@ class InternalTests(unittest.TestCase):
             PkgConfigDependency.check_pkgconfig = old_check
             # Reset dependency class to ensure that in-process configure doesn't mess up
             PkgConfigDependency.pkgbin_cache = {}
-            PkgConfigDependency.class_pkgbin = None
+            PkgConfigDependency.class_pkgbin = old_pkgbin
 
     def test_version_compare(self):
         comparefunc = mesonbuild.mesonlib.version_compare_many
@@ -4843,7 +4850,7 @@ class NativeFileTests(BasePlatformTests):
             getter = functools.partial(getter, False)
         cc = getter()
         binary, newid = cb(cc)
-        env.config_info.binaries = {lang: binary}
+        env.binaries.host.binaries[lang] = binary
         compiler = getter()
         self.assertEqual(compiler.id, newid)
 
@@ -4998,7 +5005,7 @@ class NativeFileTests(BasePlatformTests):
         getter = getattr(env, 'detect_{}_compiler'.format(lang))
         if lang in ['rust']:
             getter = functools.partial(getter, False)
-        env.config_info.binaries = {lang: wrapper}
+        env.binaries.host.binaries[lang] = wrapper
         compiler = getter()
         self.assertEqual(compiler.version, version)
 
@@ -5024,7 +5031,7 @@ class NativeFileTests(BasePlatformTests):
         wrapper = self.helper_create_binary_wrapper(
             'swiftc', version='Swift 1.2345', outfile='stderr')
         env = get_fake_env('', '', '')
-        env.config_info.binaries = {'swift': wrapper}
+        env.binaries.host.binaries['swift'] = wrapper
         compiler = env.detect_swift_compiler()
         self.assertEqual(compiler.version, '1.2345')