Repository: conan-io/conan
Pull request: Feature : cross-building tests for new AutoTools build helper
closes: #8762
Changelog: Feature: Add cross-building tests for new AutoTools build helper.
Docs: omit

- [x] Refer to the issue that supports this Pull Request.
- [x] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request.
- [x] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've followed the PEP8 style guides for Python code.
- [ ] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one. 

<sup>**Note:** By default this PR will skip the slower tests and will use a limited set of python versions. Check [here](https://github.com/conan-io/conan/blob/develop/.github/PR_INCREASE_TESTING.md) how to increase the testing level by writing some tags in the current PR body text.</sup>


Problem statement:
[feature] Implementation of cross-build triplets in new conan.tools.gnu AutotoolsToolchain
- Triplets must be defined at toolchain time (``generate()``), and stored in a file or any mechanism that makes as easy as possible to be used by users. In the worst case, yes, copy&paste from file to avoid retyping.
- The ``Autootools()`` helper in the ``build()`` method will just use this from the result of ``generate()``
- Use exclusively the build/host contexts and profiles. No usage of any detection or current platform, everything must come from profiles.
- If necessary use the new [conf] to define extra complements, but try to avoid as much as possible and draft a minimalistic approach
- It is fine if it only starts with build/host but not the target one
- Try to include a functional test that really cross-build, even if it doesn't pass CI now (we can talk to @czoido to install something, should be the easiest to install cross-compiler), you can use an @pytest.mark.xfail at the moment

Hints from issue discussion:
Please @SSE4 use the new ``CONAN_TOOLCHAIN_ARGS_FILE = "conanbuild.json"`` file to gather variables for ``Autotools`` instead of separate files, and lets use that accross build systems. Otherwise it is becoming a bit cluttered.

Commit message: - cross-building tests for new AutoTools build helper

Signed-off-by: SSE4 <tomskside@gmail.com>

Patch to review:
diff --git a/conan/tools/gnu/autotools.py b/conan/tools/gnu/autotools.py
index 6c912487b43..fd0329e3d1e 100644
--- a/conan/tools/gnu/autotools.py
+++ b/conan/tools/gnu/autotools.py
@@ -1,6 +1,9 @@
-import platform
+import json
+import os
 
+from conan.tools import CONAN_TOOLCHAIN_ARGS_FILE
 from conan.tools._compilers import use_win_mingw
+from conans.util.files import load
 
 
 class Autotools(object):
@@ -20,9 +23,14 @@ def __init__(self, conanfile):
         self._build_type = conanfile.settings.get_safe("build_type")
         self._compiler = conanfile.settings.get_safe("compiler")
         self._compiler_version = conanfile.settings.get_safe("compiler.version")
-
-        # Precalculate build, host, target triplets
-        # TODO self.build, self.host, self.target = self._get_host_build_target_flags()
+        self._build = None
+        self._host = None
+        self._target = None
+        if os.path.isfile(CONAN_TOOLCHAIN_ARGS_FILE):
+            args = json.loads(load(CONAN_TOOLCHAIN_ARGS_FILE))
+            self._build = args["build"] if "build" in args else None
+            self._host = args["host"] if "host" in args else None
+            self._target = args["target"] if "target" in args else None
 
     def configure(self):
         """
@@ -33,11 +41,13 @@ def configure(self):
             return
         configure_dir = "."
 
-        # TODO: Management of build, host, target triplet
         # TODO: Management of PKG_CONFIG_PATH
         # TODO: Implement management of --prefix, bindir, sbindir, libexecdir, libdir, includedir
 
         cmd = "%s/configure" % configure_dir
+        cmd += ' --host=%s' % self._host if self._host else ''
+        cmd += ' --build=%s' % self._build if self._build else ''
+        cmd += ' --target=%s' % self._target if self._target else ''
         self._conanfile.output.info("Calling:\n > %s" % cmd)
         self._conanfile.run(cmd)
 
diff --git a/conan/tools/gnu/autotoolsgen.py b/conan/tools/gnu/autotoolsgen.py
index ac0b523fefc..ebe2d95b8be 100644
--- a/conan/tools/gnu/autotoolsgen.py
+++ b/conan/tools/gnu/autotoolsgen.py
@@ -33,3 +33,5 @@ def generate(self):
         else:
             build_env.save_sh("conanbuildenv.sh")
             run_env.save_sh("conanrunenv.sh")
+
+        self.toolchain.generate()
diff --git a/conan/tools/gnu/autotoolstoolchain.py b/conan/tools/gnu/autotoolstoolchain.py
index c9358d85f70..d76704d8d5b 100644
--- a/conan/tools/gnu/autotoolstoolchain.py
+++ b/conan/tools/gnu/autotoolstoolchain.py
@@ -1,7 +1,12 @@
+import json
+
+from conan.tools import CONAN_TOOLCHAIN_ARGS_FILE
 from conan.tools._compilers import architecture_flag, build_type_flags
 from conan.tools.env import Environment
 # FIXME: need to refactor this import and bring to conan.tools
 from conans.client.build.cppstd_flags import cppstd_flag_new
+from conans.client.tools.oss import cross_building, get_cross_building_settings, get_gnu_triplet
+from conans.util.files import save
 
 
 class AutotoolsToolchain:
@@ -30,6 +35,15 @@ def __init__(self, conanfile):
         # TODO: This is also covering compilers like Visual Studio, necessary to test it (&remove?)
         self.build_type_flags = build_type_flags(self._conanfile.settings)
 
+        self._host = None
+        self._build = None
+        self._target = None
+
+        if cross_building(self._conanfile):
+            os_build, arch_build, os_host, arch_host = get_cross_building_settings(self._conanfile)
+            self._host = get_gnu_triplet(os_host, arch_host)
+            self._build = get_gnu_triplet(os_build, arch_build)
+
     def _rpaths_link(self):
         # TODO: Not implemented yet
         pass
@@ -109,3 +123,9 @@ def generate(self):
         env = self.environment()
         env.save_sh("conanautotoolstoolchain.sh")
         env.save_bat("conanautotoolstoolchain.bat")
+
+        args = {"build": self._build,
+                "host": self._host,
+                "target": self._target}
+        args = {k: v for k, v in args.items() if v is not None}
+        save(CONAN_TOOLCHAIN_ARGS_FILE, json.dumps(args))
diff --git a/conans/test/assets/autotools.py b/conans/test/assets/autotools.py
index d074cf11cb9..473b291d252 100644
--- a/conans/test/assets/autotools.py
+++ b/conans/test/assets/autotools.py
@@ -14,7 +14,9 @@
 {% if main and lib %}
 {{main}}_LDADD = {{ lib }}
 {% endif %}
+
 """
+# newline at the end is important: m4: INTERNAL ERROR: recursive push_string!
 
 
 def gen_makefile_am(**context):
@@ -30,7 +32,9 @@ def gen_makefile_am(**context):
 AM_PROG_AR
 AC_CONFIG_FILES([Makefile])
 AC_OUTPUT
+
 """
+# newline at the end is important: m4: INTERNAL ERROR: recursive push_string!
 
 
 def gen_configure_ac(**context):
diff --git a/conans/test/functional/toolchains/gnu/test_autotools.py b/conans/test/functional/toolchains/gnu/test_autotools.py
index 59f7afeb06d..8bf518023d3 100644
--- a/conans/test/functional/toolchains/gnu/test_autotools.py
+++ b/conans/test/functional/toolchains/gnu/test_autotools.py
@@ -13,7 +13,7 @@
 from conans.util.files import touch
 
 
-@pytest.mark.skipif(platform.system() != "Linux", reason="Requires Autotools")
+@pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Requires Autotools")
 @pytest.mark.tool_autotools()
 def test_autotools():
     client = TestClient(path_with_spaces=False)
@@ -50,7 +50,8 @@ def build(self):
     client.run("install .")
     client.run("build .")
     client.run_command("./main")
-    check_exe_run(client.out, "main", "gcc", None, "Release", "x86_64", None, cxx11_abi=0)
+    cxx11_abi = 0 if platform.system() == "Linux" else None
+    check_exe_run(client.out, "main", "gcc", None, "Release", "x86_64", None, cxx11_abi=cxx11_abi)
     assert "hello/0.1: Hello World Release!" in client.out
 
 
diff --git a/conans/test/functional/toolchains/gnu/test_ios.py b/conans/test/functional/toolchains/gnu/test_ios.py
new file mode 100644
index 00000000000..bb5191ff3ff
--- /dev/null
+++ b/conans/test/functional/toolchains/gnu/test_ios.py
@@ -0,0 +1,77 @@
+import platform
+import textwrap
+
+import pytest
+
+from conans.client.tools.apple import XCRun, to_apple_arch
+from conans.test.assets.autotools import gen_makefile_am, gen_configure_ac
+from conans.test.assets.sources import gen_function_cpp
+from conans.test.utils.tools import TestClient
+
+
+@pytest.mark.skipif(platform.system() != "Darwin", reason="Requires Xcode")
+def test_ios():
+    xcrun = XCRun(None, sdk='iphoneos')
+    cflags = ""
+    cflags += " -isysroot " + xcrun.sdk_path
+    cflags += " -arch " + to_apple_arch('armv8')
+    cxxflags = cflags
+    ldflags = cflags
+
+    profile = textwrap.dedent("""
+        include(default)
+        [settings]
+        os=iOS
+        os.version=12.0
+        arch=armv8
+        [env]
+        CC={cc}
+        CXX={cxx}
+        CFLAGS={cflags}
+        CXXFLAGS={cxxflags}
+        LDFLAGS={ldflags}
+    """).format(cc=xcrun.cc, cxx=xcrun.cxx, cflags=cflags, cxxflags=cxxflags, ldflags=ldflags)
+
+    client = TestClient(path_with_spaces=False)
+    client.save({"m1": profile}, clean_first=True)
+    client.run("new hello/0.1 --template=v2_cmake")
+    client.run("create . --profile:build=default --profile:host=m1 -tf None")
+
+    main = gen_function_cpp(name="main", includes=["hello"], calls=["hello"])
+    makefile_am = gen_makefile_am(main="main", main_srcs="main.cpp")
+    configure_ac = gen_configure_ac()
+
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        from conan.tools.gnu import Autotools
+
+        class TestConan(ConanFile):
+            requires = "hello/0.1"
+            settings = "os", "compiler", "arch", "build_type"
+            exports_sources = "configure.ac", "Makefile.am", "main.cpp"
+            generators = "AutotoolsGen"
+
+            def build(self):
+                self.run("aclocal")
+                self.run("autoconf")
+                self.run("automake --add-missing --foreign")
+                autotools = Autotools(self)
+                autotools.configure()
+                autotools.make()
+        """)
+
+    client.save({"conanfile.py": conanfile,
+                 "configure.ac": configure_ac,
+                 "Makefile.am": makefile_am,
+                 "main.cpp": main,
+                 "m1": profile}, clean_first=True)
+    client.run("install . --profile:build=default --profile:host=m1")
+    client.run("build .")
+    client.run_command("./main", assert_error=True)
+    assert "Bad CPU type in executable" in client.out
+    client.run_command("lipo -info main")
+    assert "Non-fat file: main is architecture: arm64" in client.out
+
+    js = client.load("conanbuild.json")
+    assert '{"build": "x86_64-apple-darwin", "host": "aarch64-apple-ios"}' in js
+
diff --git a/conans/test/functional/utils.py b/conans/test/functional/utils.py
index ca38ff5e7d0..448055743a5 100644
--- a/conans/test/functional/utils.py
+++ b/conans/test/functional/utils.py
@@ -46,20 +46,33 @@ def check_exe_run(output, names, compiler, version, build_type, arch, cppstd, de
             assert "{} _MSC_VER{}".format(name, version.replace(".", "")) in output
             assert "{} _MSVC_LANG20{}".format(name, cppstd) in output
 
-        elif compiler == "gcc":
-            assert "{} __GNUC__".format(name) in output
-
+        elif compiler in ["gcc", "clang", "apple-clang"]:
+            if compiler == "gcc":
+                assert "{} __GNUC__".format(name) in output
+                if version:  # FIXME: At the moment, the GCC version is not controlled, will change
+                    major, minor = version.split(".")[0:2]
+                    assert "{} __GNUC__{}".format(name, major) in output
+                    assert "{} __GNUC_MINOR__{}".format(name, minor) in output
+            elif compiler == "clang":
+                assert "{} __clang__".format(name) in output
+                if version:
+                    major, minor = version.split(".")[0:2]
+                    assert "{} __clang_major__{}".format(name, major) in output
+                    assert "{} __clang_minor__{}".format(name, minor) in output
+            elif compiler == "apple-clang":
+                assert "{} __apple_build_version__".format(name) in output
+                if version:
+                    major, minor = version.split(".")[0:2]
+                    assert "{} __apple_build_version__{}{}".format(name, major, minor) in output
             if arch == "x86":
                 assert "{} __i386__ defined".format(name) in output
             elif arch == "x86_64":
                 assert "{} __x86_64__ defined".format(name) in output
+            elif arch == "armv8":
+                assert "{} __aarch64__ defined".format(name) in output
             else:
                 assert arch is None, "checked don't know how to validate this architecture"
 
-            if version:  # FIXME: At the moment, the GCC version is not controlled, will change
-                major, minor = version.split(".")[0:2]
-                assert "{} __GNUC__{}".format(name, major) in output
-                assert "{} __GNUC_MINOR__{}".format(name, minor) in output
             if cppstd:
                 cppstd_value = {"98": "199711",
                                 "11": "201103",
@@ -70,10 +83,6 @@ def check_exe_run(output, names, compiler, version, build_type, arch, cppstd, de
             if cxx11_abi is not None:
                 assert "{} _GLIBCXX_USE_CXX11_ABI {}".format(name, cxx11_abi) in output
 
-        elif compiler == "apple-clang":
-            # TODO: apple-clang requires checks too
-            pass
-
         if definitions:
             for k, v in definitions.items():
                 assert "{}: {}".format(k, v) in output


Code review comments:
diff_hunk: @@ -1,7 +1,12 @@
+import json
+
+from conan.tools import CONAN_TOOLCHAIN_ARGS_FILE
 from conan.tools._compilers import architecture_flag, build_type_flags
 from conan.tools.env import Environment
 # FIXME: need to refactor this import and bring to conan.tools
 from conans.client.build.cppstd_flags import cppstd_flag_new
+from conans.client.tools.oss import cross_building, get_cross_building_settings, get_gnu_triplet
path: conan/tools/gnu/autotoolstoolchain.py
text: @user1:
In general try to avoid things from old ``conans.client....`` scope. The ``cross_building, get_cross_building_settings, get_gnu_triplet`` should be copied here, because they are going to be modernized. This one already has a FIXME above, so it can be done in a later PR, but at least the idea for other PRs.

@author:
okay, let's copy them

@author:
made a copies of `cross_building`, `get_cross_building_settings`, `get_gnu_triplet` into `conan.tools.oss`. it's like a spider web, one method pulling another one from the `conans.client.tools`, so I had to cut `OSInfo` to only used methods to avoid copying almost everything now (e.g. it requires `which`, `environment_append` and so on for full functionality, it actually does to much - classic god object).

@user1:
yes, this is one of the purposes of doing this copy. All this should be distilled and reduced much, specially if using both profiles, and not needing to detect anything at all. This will follow in future PRs.

diff_hunk: @@ -33,3 +33,5 @@ def generate(self):
         else:
             build_env.save_sh("conanbuildenv.sh")
             run_env.save_sh("conanrunenv.sh")
+
+        self.toolchain.generate()
path: conan/tools/gnu/autotoolsgen.py
text: @user1:
This is confusing, as it will generate an extra, unused file. 
Move the AutotoolsToolchain CONAN_TOOLCHAIN_ARGS_FILE functionality to its own method, and call just that method.

@author:
I am not sure I got the idea behind the new method, but I'll do

@user1:
To avoid the creation of an extra ``conanautotooltoolchain.sh`` script that will not be used.

@author:
okay, I've extracted the method `generate_args`.
still would be nice if you could explain your idea a bit more, since for CMake toolchain we don't do a file, it just writes `CONAN_TOOLCHAIN_ARGS_FILE` inside the `generate`.
I am not sure what is an extra and unused file, as `CONAN_TOOLCHAIN_ARGS_FILE` is definitely used.
and as I understand the point of `generate` is for developer to have exactly all the files needed, so I am not sure why some files belong to `generate`, but some don't.