Repository: conan-io/conan

Issue:
[question] Why do we skip 64/32 cross compilation with meson?
See https://github.com/conan-io/conan/blob/d35a73142a538808083f775ba3108664e1d50415/conan/tools/meson/toolchain.py#L159C2-L159C73

We are trying to cross build glib for linux x64->x86 (`arch=x86`) using the official conancenter recipe and the host profile
```
[settings]
os=Linux
arch=x86
compiler=clang
compiler.version=14
compiler.libcxx=libstdc++11
build_type=Debug
```


This leads to the following error:
`/usr/bin/ld: i386 architecture of input file '/conan/.conan/data/pcre2/10.42/_/_/package/9ff55d811dff94adb5ea6675dae30929f0465371/lib/libpcre2-8.a(pcre2_chartables.c.o)' is incompatible with i386:x86-64 output`
see [glib_error_logs.txt](https://github.com/user-attachments/files/17588545/glib_error_logs.txt)

It can be fixed by setting `tools.build.cross_building:cross_build=True`, however shouldn't this work out of the box without any special configuration?


Discussion:
Thanks for your question @mattarroz 

In general, Windows compilation to x86 is not considered cross-compilation, but a native one, for other build systems.
Maybe Meson is different here and needs to change that? Do you have any reference from Meson that could clarify this?

If that is the case, maybe the fix is to do ``cross_building(conanfile, skip_x64_x86=False)`` for Meson instead?
> Thanks for your question @mattarroz
> 
> In general, Windows compilation to x86 is not considered cross-compilation, but a native one, for other build systems. Maybe Meson is different here and needs to change that? Do you have any reference from Meson that could clarify this?

Meson's cross-compilation documentation at least mentions x86 as an exemplary host architecture (see [here](https://mesonbuild.com/Cross-compilation.html#machine-entries)).



> 
> If that is the case, maybe the fix is to do `cross_building(conanfile, skip_x64_x86=False)` for Meson instead?

Given that we had success explicitly overriding `skip_x64_x86=True` through `tools.build.cross_building:cross_build=True`, my guess is that setting it to `False` is the way to go. However, I am not a meson expert.


Maybe @eli-schwartz can give here some feedback?
Meson isn't strongly opinionated about whether booting into x86_64 and running an i686 compiler to produce i686 binaries should be considered a cross build or a native build.

If you specify an i686 compiler using `--cross-file=` then meson will configure its cross architecture and detect a "native" x86_64 compiler for targets marked for native compilation (`native: true`, e.g. code generator tools written in C/C++ that do stuff like dump tables or data files in order to produce a header for the rest of the build). And it will use the i686 compiler to produce the installable artifacts.

If you specify an i686 compiler as the regular $CC then meson will simply regard your current build as an i686 userland running on an x86_64 kernel. This isn't even particularly unusual, there have been times when I installed an i686 linux distro and then manually installed their x86_64 kernel package while ignoring the configured system architecture, so I could execute 64-bit statically linked programs even though my operating system is i686. It's also how running an i686 container or chroot for CI purposes, works.

...

It is possible to have an x86_64 kernel that does NOT support i686 emulation. Your only option there would be configuring as a cross build.
Thanks for the feedback @eli-schwartz 

It seems that I was thinking mostly about Windows, but it is true that nowadays there are Linux systems that might not support the i686 modes, and for those, the current default assumption ``cross_building(conanfile, skip_x64_x86=True)`` wouldn't hold. 

But it seems that changing it wouldn't be the best way to proceed, as it will definitely break users that depend on this behavior for their platforms that do support i686 mode, so it is possible that the recommended approach for this is to define the ``cross_build=True`` configuration explicitly (this is one of the reasons this configuration exists).
> Thanks for the feedback @eli-schwartz
> 
> It seems that I was thinking mostly about Windows, but it is true that nowadays there are Linux systems that might not support the i686 modes, and for those, the current default assumption `cross_building(conanfile, skip_x64_x86=True)` wouldn't hold.
> 
> But it seems that changing it wouldn't be the best way to proceed, as it will definitely break users that depend on this behavior for their platforms that do support i686 mode, so it is possible that the recommended approach for this is to define the `cross_build=True` configuration explicitly (this is one of the reasons this configuration exists).

From an UX point of view, I believe we should improve here. Conan supports cross building by specifying build and host profile, so it seems very counter-intuitive still having to explicitly enable cross building by setting `cross_build=True`. 

I know this might be an edge case, but let's improve usability and at least document this behavior. The documentation explicitly states `skip_x64_x86=False` as the default value: https://docs.conan.io/2/reference/tools/build.html#conan-tools-build-cross-building
It should be mentioned that this isn't the case for meson, shouldn't it be? If that was the case, a good explanation on why the meson default value differs from other build tools should be given.

Maybe also a warning/info message should be implemented to advise the user that this setting is necessary for 64/32 bit cross builds.


profile is changed accordingly:
```
[settings]
os=Linux
arch=x86
arch_build=x86_64
compiler=clang
compiler.version=14
compiler.libcxx=libstdc++11
build_type=Debug

[env]
CONAN_PROFILE_NAME=clang14_linux_x86_debug
CC=clang-14
CXX=clang++-14


#[buildenv]
#CFLAGS=-m32
#CXXFLAGS=-m32
#LDFLAGS=-m32

[conf]
tools.build.cross_building:cross_build=True
```
explicitly specifying conf to build for cross build, but flags were commented out.
Meson is generating a cross file.

> glib/2.81.0: Meson configure cmd: meson setup --cross-file "/conan/.conan/data/glib/2.81.0/_/_/build/1396172a72a68b0a2f8810cb73fc9927107ea0a3/build-release/conan/conan_meson_cross.ini" "/conan/.conan/data/glib/2.81.0/_/_/build/1396172a72a68b0a2f8810cb73fc9927107ea0a3/build-release" "/conan/.conan/data/glib/2.81.0/_/_/build/1396172a72a68b0a2f8810cb73fc9927107ea0a3/src" --prefix=/

detailed logs are attached in file.
[glib_32bit_logs.txt](https://github.com/user-attachments/files/17598004/glib_32bit_logs.txt)

Even after meson tool has generate a cross-file, confusing why still, 64-bit binaries are being generated?
for reference, attaching cross file which is generated.
[conan_meson_cross.txt](https://github.com/user-attachments/files/17598060/conan_meson_cross.txt)

But in commands of ninja there is no flag `-m32` being added implicitly.
In the [buildenv] if flags are mentioned explicitly, only then, the binaries are being built for 32 bit. why the flags are not being added implicitly once the cross build is recognized?
> From an UX point of view, I believe we should improve here. Conan supports cross building by specifying build and host profile, so it seems very counter-intuitive still having to explicitly enable cross building by setting cross_build=True.

The issue is that having build and host profile doesn't imply it is a cross-build. In Conan 2, the 2 build/host profiles are always used. And there are many cases with different build and host profiles that are not cross-build, for example:
- When using ``build_type=Debug``, almost always, the "build" context will be still in ``build_type=Release``
- It is very common that the dependencies of the "build" context are ``shared=False``, linked statically, even if the host context uses ``shared=True`` dynamic libraries

So having different build-host profiles is not enough to guarantee that it is a cross-build.

> It should be mentioned that this isn't the case for meson, shouldn't it be? If that was the case, a good explanation on why the meson default value differs from other build tools should be given.

That is indeed a relevant point. If the other build systems have other default, and Meson is not opinionated on this, I think it should be changed to align (could be considered a bug)
Proposing https://github.com/conan-io/conan/pull/17266 to see if it breaks some exiting tests (would need some new tests to cover the change)



Patch:
diff --git a/conan/tools/meson/toolchain.py b/conan/tools/meson/toolchain.py
index 743d38cbb25..363861b8ec0 100644
--- a/conan/tools/meson/toolchain.py
+++ b/conan/tools/meson/toolchain.py
@@ -16,7 +16,7 @@
 from conans.util.files import save
 
 
-class MesonToolchain(object):
+class MesonToolchain:
     """
     MesonToolchain generator
     """
@@ -156,7 +156,7 @@ def __init__(self, conanfile, backend=None, native=False):
         self._conanfile = conanfile
         self._native = native
         self._is_apple_system = is_apple_os(self._conanfile)
-        is_cross_building = cross_building(conanfile, skip_x64_x86=True)
+        is_cross_building = cross_building(conanfile)  # x86_64->x86 is considered cross-building
         if not is_cross_building and native:
             raise ConanException("You can only pass native=True if you're cross-building, "
                                  "otherwise, it could cause unexpected results.")
@@ -164,8 +164,8 @@ def __init__(self, conanfile, backend=None, native=False):
         # Values are kept as Python built-ins so users can modify them more easily, and they are
         # only converted to Meson file syntax for rendering
         # priority: first user conf, then recipe, last one is default "ninja"
-        self._backend = conanfile.conf.get("tools.meson.mesontoolchain:backend",
-                                           default=backend or 'ninja')
+        self._backend = self._conanfile_conf.get("tools.meson.mesontoolchain:backend",
+                                                 default=backend or 'ninja')
         build_type = self._conanfile.settings.get_safe("build_type")
         self._buildtype = {"Debug": "debug",  # Note, it is not "'debug'"
                            "Release": "release",
@@ -424,13 +424,13 @@ def _get_extra_flags(self):
         exelinkflags = self._conanfile_conf.get("tools.build:exelinkflags", default=[], check_type=list)
         linker_scripts = self._conanfile_conf.get("tools.build:linker_scripts", default=[], check_type=list)
         linker_script_flags = ['-T"' + linker_script + '"' for linker_script in linker_scripts]
-        defines = self._conanfile.conf.get("tools.build:defines", default=[], check_type=list)
+        defines = self._conanfile_conf.get("tools.build:defines", default=[], check_type=list)
         sys_root = [f"--sysroot={self._sys_root}"] if self._sys_root else [""]
+        ld = sharedlinkflags + exelinkflags + linker_script_flags + sys_root + self.extra_ldflags
         return {
             "cxxflags": cxxflags + sys_root + self.extra_cxxflags,
             "cflags": cflags + sys_root + self.extra_cflags,
-            "ldflags": sharedlinkflags + exelinkflags + linker_script_flags
-                       + sys_root + self.extra_ldflags,
+            "ldflags": ld,
             "defines": [f"-D{d}" for d in (defines + self.extra_defines)]
         }
 
@@ -447,11 +447,12 @@ def _filter_list_empty_fields(v):
     def _sanitize_env_format(value):
         if value is None or isinstance(value, list):
             return value
+        if not isinstance(value, str):
+            raise ConanException(f"MesonToolchain: Value '{value}' should be a string")
         ret = [x.strip() for x in value.split() if x]
         return ret[0] if len(ret) == 1 else ret
 
     def _context(self):
-
         apple_flags = self.apple_isysroot_flag + self.apple_arch_flag + self.apple_min_version_flag
         extra_flags = self._get_extra_flags()
 
@@ -474,10 +475,11 @@ def _context(self):
 
         subproject_options = {}
         for subproject, listkeypair in self.subproject_options.items():
-            if listkeypair is not None and listkeypair is not []:
-                subproject_options[subproject] = []
-                for keypair in listkeypair:
-                    subproject_options[subproject].append({k: to_meson_value(v) for k, v in keypair.items()})
+            if listkeypair:
+                if not isinstance(listkeypair, list):
+                    raise ConanException("MesonToolchain.subproject_options must be a list of dicts")
+                subproject_options[subproject] = [{k: to_meson_value(v) for k, v in keypair.items()}
+                                                  for keypair in listkeypair]
 
         return {
             # https://mesonbuild.com/Machine-files.html#properties
@@ -555,6 +557,7 @@ def generate(self):
         If Windows OS, it will be created a ``conanvcvars.bat`` as well.
         """
         check_duplicated_generator(self, self._conanfile)
+        self._conanfile.output.info(f"MesonToolchain generated: {self._filename}")
         save(self._filename, self._content)
         # FIXME: Should we check the OS and compiler to call VCVars?
         VCVars(self._conanfile).generate()


Test patch:
diff --git a/test/integration/toolchains/meson/test_mesontoolchain.py b/test/integration/toolchains/meson/test_mesontoolchain.py
index d9ca23ea9e4..a9075c66785 100644
--- a/test/integration/toolchains/meson/test_mesontoolchain.py
+++ b/test/integration/toolchains/meson/test_mesontoolchain.py
@@ -7,7 +7,6 @@
 
 from conan.test.assets.genconanfile import GenConanfile
 from conan.test.utils.tools import TestClient
-from conan.tools.files import load
 from conan.tools.meson import MesonToolchain
 
 
@@ -540,8 +539,8 @@ def generate(self):
                  "build": build,
                  "conanfile.py": conanfile})
     client.run("install . -pr:h host -pr:b build")
-    native_content = load(None, os.path.join(client.current_folder, MesonToolchain.native_filename))
-    cross_content = load(None, os.path.join(client.current_folder, MesonToolchain.cross_filename))
+    native_content = client.load(MesonToolchain.native_filename)
+    cross_content = client.load(MesonToolchain.cross_filename)
     expected_native = textwrap.dedent("""
     [binaries]
     c = 'clang'
@@ -640,9 +639,23 @@ def test_meson_sysroot_app():
                  "host": profile})
     client.run("install . -pr:h host -pr:b build")
     # Check the meson configuration file
-    conan_meson = client.load(os.path.join(client.current_folder, "conan_meson_cross.ini"))
+    conan_meson = client.load("conan_meson_cross.ini")
     assert f"sys_root = '{sysroot}'\n" in conan_meson
     assert re.search(r"c_args =.+--sysroot={}.+".format(sysroot), conan_meson)
     assert re.search(r"c_link_args =.+--sysroot={}.+".format(sysroot), conan_meson)
     assert re.search(r"cpp_args =.+--sysroot={}.+".format(sysroot), conan_meson)
     assert re.search(r"cpp_link_args =.+--sysroot={}.+".format(sysroot), conan_meson)
+
+
+def test_cross_x86_64_to_x86():
+    """
+    https://github.com/conan-io/conan/issues/17261
+    """
+
+    c = TestClient()
+    c.save({"conanfile.py": GenConanfile().with_settings("os", "compiler", "arch", "build_type")})
+    c.run("install . -g MesonToolchain -s arch=x86 -s:b arch=x86_64")
+    assert not os.path.exists(os.path.join(c.current_folder, MesonToolchain.native_filename))
+    cross = c.load(MesonToolchain.cross_filename)
+    assert "cpu = 'x86_64'" in cross  # This is the build machine
+    assert "cpu = 'x86'" in cross  # This is the host machine