Repository: conan-io/conan

Issue:
[feature] Create "new --template" for Autotools.
Like https://github.com/conan-io/conan/pull/10760/files  but for Autotools.
Tests using it, even with windows subsystems.
Better study if a predefined layout is convenient. 


Discussion:

Patch:
diff --git a/conan/tools/gnu/autotools.py b/conan/tools/gnu/autotools.py
index 8776e6dd7..af335a46b 100644
--- a/conan/tools/gnu/autotools.py
+++ b/conan/tools/gnu/autotools.py
@@ -4,28 +4,47 @@ from conan.tools.build import build_jobs
 from conan.tools.files.files import load_toolchain_args
 from conans.client.subsystems import subsystem_path, deduce_subsystem
 from conans.client.build import join_arguments
+from conans.tools import args_to_string
+from conan.tools.files import chdir
 
 
 class Autotools(object):
 
-    def __init__(self, conanfile, namespace=None):
+    def __init__(self, conanfile, namespace=None, build_script_folder=None):
         self._conanfile = conanfile
 
         toolchain_file_content = load_toolchain_args(self._conanfile.generators_folder,
                                                      namespace=namespace)
         self._configure_args = toolchain_file_content.get("configure_args")
         self._make_args = toolchain_file_content.get("make_args")
+        self.default_configure_install_args = True
+        self.build_script_folder = os.path.join(self._conanfile.source_folder, build_script_folder) \
+            if build_script_folder else self._conanfile.source_folder
 
-    def configure(self, build_script_folder=None):
+    def configure(self):
         """
         http://jingfenghanmax.blogspot.com.es/2010/09/configure-with-host-target-and-build.html
         https://gcc.gnu.org/onlinedocs/gccint/Configure-Terms.html
         """
-        source = self._conanfile.source_folder
-        if build_script_folder:
-            source = os.path.join(self._conanfile.source_folder, build_script_folder)
+        configure_args = []
+        if self.default_configure_install_args and self._conanfile.package_folder:
+            def _get_argument(argument_name, cppinfo_name):
+                elements = getattr(self._conanfile.cpp.package, cppinfo_name)
+                return "--{}=${{prefix}}/{}".format(argument_name, elements[0]) if elements else ""
 
-        configure_cmd = "{}/configure".format(source)
+            # If someone want arguments but not the defaults can pass them in args manually
+            configure_args.extend(["--prefix=%s" % self._conanfile.package_folder.replace("\\", "/"),
+                                   _get_argument("bindir", "bindirs"),
+                                   _get_argument("sbindir", "bindirs"),
+                                   _get_argument("libdir", "libdirs"),
+                                   _get_argument("includedir", "includedirs"),
+                                   _get_argument("oldincludedir", "includedirs"),
+                                   _get_argument("datarootdir", "resdirs")])
+
+        self._configure_args = "{} {}".format(self._configure_args, args_to_string(configure_args)) \
+                               if configure_args else self._configure_args
+
+        configure_cmd = "{}/configure".format(self.build_script_folder)
         subsystem = deduce_subsystem(self._conanfile, scope="build")
         configure_cmd = subsystem_path(subsystem, configure_cmd)
         cmd = '"{}" {}'.format(configure_cmd, self._configure_args)
@@ -45,8 +64,19 @@ class Autotools(object):
         self._conanfile.run(command)
 
     def install(self):
+        # FIXME: we have to run configure twice because the local flow won't work otherwise
+        #  because there's no package_folder until the package step
+        self.configure()
         self.make(target="install")
 
+    def autoreconf(self, args=None):
+        command = ["autoreconf"]
+        args = args or ["--force", "--install"]
+        command.extend(args)
+        command = join_arguments(command)
+        with chdir(self, self._conanfile.source_folder):
+            self._conanfile.run(command)
+
     def _use_win_mingw(self):
         if hasattr(self._conanfile, 'settings_build'):
             os_build = self._conanfile.settings_build.get_safe('os')
diff --git a/conan/tools/gnu/autotoolstoolchain.py b/conan/tools/gnu/autotoolstoolchain.py
index 85deead7e..d859bd655 100644
--- a/conan/tools/gnu/autotoolstoolchain.py
+++ b/conan/tools/gnu/autotoolstoolchain.py
@@ -8,6 +8,7 @@ from conan.tools.env import Environment
 from conan.tools.files.files import save_toolchain_args
 from conan.tools.gnu.get_gnu_triplet import _get_gnu_triplet
 from conan.tools.microsoft import VCVars, is_msvc, msvc_runtime_flag
+from conans.errors import ConanException
 from conans.tools import args_to_string
 
 
@@ -18,7 +19,6 @@ class AutotoolsToolchain:
 
         self.configure_args = []
         self.make_args = []
-        self.default_configure_install_args = True
 
         # Flags
         self.cxxflags = []
@@ -166,26 +166,20 @@ class AutotoolsToolchain:
         self.generate_args()
         VCVars(self._conanfile).generate(scope=scope)
 
+    def _shared_static_args(self):
+        args = []
+        if self._conanfile.options.get_safe("shared", False):
+            args.extend(["--enable-shared", "--disable-static"])
+        else:
+            args.extend(["--disable-shared", "--enable-static", "--with-pic"
+                        if self._conanfile.options.get_safe("fPIC", True)
+                        else "--without-pic"])
+        return args
+
     def generate_args(self):
         configure_args = []
+        configure_args.extend(self._shared_static_args())
         configure_args.extend(self.configure_args)
-
-        if self.default_configure_install_args and self._conanfile.package_folder:
-            def _get_cpp_info_value(name):
-                # Why not taking cpp.build? because this variables are used by the "cmake install"
-                # that correspond to the package folder (even if the root is the build directory)
-                elements = getattr(self._conanfile.cpp.package, name)
-                return elements[0] if elements else None
-
-            # If someone want arguments but not the defaults can pass them in args manually
-            configure_args.extend(
-                    ['--prefix=%s' % self._conanfile.package_folder.replace("\\", "/"),
-                     "--bindir=${prefix}/%s" % _get_cpp_info_value("bindirs"),
-                     "--sbindir=${prefix}/%s" % _get_cpp_info_value("bindirs"),
-                     "--libdir=${prefix}/%s" % _get_cpp_info_value("libdirs"),
-                     "--includedir=${prefix}/%s" % _get_cpp_info_value("includedirs"),
-                     "--oldincludedir=${prefix}/%s" % _get_cpp_info_value("includedirs"),
-                     "--datarootdir=${prefix}/%s" % _get_cpp_info_value("resdirs")])
         user_args_str = args_to_string(self.configure_args)
         for flag, var in (("host", self._host), ("build", self._build), ("target", self._target)):
             if var and flag not in user_args_str:
diff --git a/conans/assets/templates/new_v2_autotools.py b/conans/assets/templates/new_v2_autotools.py
new file mode 100644
index 000000000..413488976
--- /dev/null
+++ b/conans/assets/templates/new_v2_autotools.py
@@ -0,0 +1,233 @@
+import textwrap
+
+from conans.assets.templates.new_v2_cmake import source_cpp, source_h, test_main
+
+conanfile_lib = textwrap.dedent("""
+    import os
+
+    from conan import ConanFile
+    from conan.tools.gnu import AutotoolsToolchain, Autotools
+    from conan.tools.layout import basic_layout
+    from conan.tools.files import chdir
+
+
+    class {package_name}Conan(ConanFile):
+        name = "{name}"
+        version = "{version}"
+
+        # Optional metadata
+        license = "<Put the package license here>"
+        author = "<Put your name here> <And your email here>"
+        url = "<Package recipe repository url here, for issues about the package>"
+        description = "<Description of {package_name} here>"
+        topics = ("<Put some tag here>", "<here>", "<and here>")
+
+        # Binary configuration
+        settings = "os", "compiler", "build_type", "arch"
+        options = {{"shared": [True, False], "fPIC": [True, False]}}
+        default_options = {{"shared": False, "fPIC": True}}
+
+        exports_sources = "configure.ac", "Makefile.am", "src/*"
+
+        def config_options(self):
+            if self.settings.os == "Windows":
+                del self.options.fPIC
+
+        def layout(self):
+            basic_layout(self)
+
+        def generate(self):
+            at_toolchain = AutotoolsToolchain(self)
+            at_toolchain.generate()
+
+        def build(self):
+            autotools = Autotools(self)
+            autotools.autoreconf()
+            autotools.configure()
+            autotools.make()
+
+        def package(self):
+            autotools = Autotools(self)
+            autotools.install()
+
+        def package_info(self):
+            self.cpp_info.libs = ["{name}"]
+    """)
+
+configure_ac = textwrap.dedent("""
+    AC_INIT([{name}], [{version}], [])
+    AM_INIT_AUTOMAKE([-Wall -Werror foreign])
+    AC_PROG_CXX
+    AM_PROG_AR
+    LT_INIT
+    AC_CONFIG_FILES([Makefile src/Makefile])
+    AC_OUTPUT
+    """)
+
+makefile_am = textwrap.dedent("""
+    SUBDIRS = src
+    """)
+
+makefile_am_lib = textwrap.dedent("""
+    lib_LTLIBRARIES = lib{name}.la
+    lib{name}_la_SOURCES = {name}.cpp {name}.h
+    lib{name}_la_HEADERS = {name}.h
+    lib{name}_ladir = $(includedir)
+    """)
+
+test_conanfile = textwrap.dedent("""
+    import os
+
+    from conan import ConanFile
+    from conan.tools.gnu import AutotoolsToolchain, Autotools, AutotoolsDeps
+    from conan.tools.layout import basic_layout
+    from conan.tools.build import cross_building
+    from conan.tools.files import chdir
+
+
+    class {package_name}TestConan(ConanFile):
+        settings = "os", "compiler", "build_type", "arch"
+        # VirtualBuildEnv and VirtualRunEnv can be avoided if "tools.env.virtualenv:auto_use" is defined
+        # (it will be defined in Conan 2.0)
+        generators = "AutotoolsDeps", "AutotoolsToolchain", "VirtualBuildEnv", "VirtualRunEnv"
+        apply_env = False
+        test_type = "explicit"
+
+        def requirements(self):
+            self.requires(self.tested_reference_str)
+
+        def build(self):
+            autotools = Autotools(self)
+            autotools.autoreconf()
+            autotools.configure()
+            autotools.make()
+
+        def layout(self):
+            basic_layout(self)
+
+        def test(self):
+            if not cross_building(self):
+                cmd = os.path.join(self.cpp.build.bindirs[0], "main")
+                self.run(cmd, env="conanrun")
+    """)
+
+test_configure_ac = textwrap.dedent("""
+    AC_INIT([main], [1.0], [])
+    AM_INIT_AUTOMAKE([-Wall -Werror foreign])
+    AC_PROG_CXX
+    AC_PROG_RANLIB
+    AM_PROG_AR
+    AC_CONFIG_FILES([Makefile])
+    AC_OUTPUT
+    """)
+
+test_makefile_am = textwrap.dedent("""
+    bin_PROGRAMS = main
+    main_SOURCES = main.cpp
+    """)
+
+
+def get_autotools_lib_files(name, version, package_name="Pkg"):
+    files = {"conanfile.py": conanfile_lib.format(name=name, version=version,
+                                                  package_name=package_name),
+             "src/Makefile.am": makefile_am_lib.format(name=name, version=version),
+             "src/{}.cpp".format(name): source_cpp.format(name=name, version=version),
+             "src/{}.h".format(name): source_h.format(name=name, version=version),
+             "configure.ac": configure_ac.format(name=name, version=version),
+             "Makefile.am": makefile_am.format(name=name, version=version),
+             "test_package/conanfile.py": test_conanfile.format(name=name, version=version,
+                                                                package_name=package_name),
+             "test_package/main.cpp": test_main.format(name=name),
+             "test_package/configure.ac": test_configure_ac.format(name=name, version=version),
+             "test_package/Makefile.am": test_makefile_am.format(name=name, version=version)}
+    return files
+
+
+conanfile_exe = textwrap.dedent("""
+    import os
+
+    from conan import ConanFile
+    from conan.tools.gnu import AutotoolsToolchain, Autotools
+    from conan.tools.layout import basic_layout
+    from conan.tools.files import chdir
+
+
+    class {package_name}Conan(ConanFile):
+        name = "{name}"
+        version = "{version}"
+
+        # Optional metadata
+        license = "<Put the package license here>"
+        author = "<Put your name here> <And your email here>"
+        url = "<Package recipe repository url here, for issues about the package>"
+        description = "<Description of {package_name} here>"
+        topics = ("<Put some tag here>", "<here>", "<and here>")
+
+        # Binary configuration
+        settings = "os", "compiler", "build_type", "arch"
+
+        # Sources are located in the same place as this recipe, copy them to the recipe
+        exports_sources = "configure.ac", "Makefile.am", "src/*"
+
+        def layout(self):
+            basic_layout(self)
+
+        def generate(self):
+            at_toolchain = AutotoolsToolchain(self)
+            at_toolchain.generate()
+
+        def build(self):
+            autotools = Autotools(self)
+            autotools.autoreconf()
+            autotools.configure()
+            autotools.make()
+
+        def package(self):
+            autotools = Autotools(self)
+            autotools.install()
+        """)
+
+test_conanfile_exe = textwrap.dedent("""
+    import os
+    from conan import ConanFile
+    from conan.tools.build import cross_building
+    from conan.tools.layout import basic_layout
+
+
+    class {package_name}TestConan(ConanFile):
+        settings = "os", "compiler", "build_type", "arch"
+        # VirtualRunEnv can be avoided if "tools.env.virtualenv:auto_use" is defined
+        # (it will be defined in Conan 2.0)
+        generators = "VirtualRunEnv"
+        apply_env = False
+        test_type = "explicit"
+
+        def requirements(self):
+            self.requires(self.tested_reference_str)
+
+        def layout(self):
+            basic_layout(self)
+
+        def test(self):
+            if not cross_building(self):
+                self.run("{name}", env="conanrun")
+    """)
+
+makefile_am_exe = textwrap.dedent("""
+    bin_PROGRAMS = {name}
+    {name}_SOURCES = main.cpp {name}.cpp {name}.h
+    """)
+
+
+def get_autotools_exe_files(name, version, package_name="Pkg"):
+    files = {"conanfile.py": conanfile_exe.format(name=name, version=version,
+                                                  package_name=package_name),
+             "src/Makefile.am": makefile_am_exe.format(name=name, version=version),
+             "src/main.cpp": test_main.format(name=name),
+             "src/{}.cpp".format(name): source_cpp.format(name=name, version=version),
+             "src/{}.h".format(name): source_h.format(name=name, version=version),
+             "configure.ac": configure_ac.format(name=name, version=version),
+             "Makefile.am": makefile_am.format(name=name, version=version),
+             "test_package/conanfile.py": test_conanfile_exe.format(name=name, version=version,
+                                                                    package_name=package_name)}
+    return files
diff --git a/conans/client/cmd/new.py b/conans/client/cmd/new.py
index 70478ba76..67e03f1bc 100644
--- a/conans/client/cmd/new.py
+++ b/conans/client/cmd/new.py
@@ -411,6 +411,12 @@ def cmd_new(ref, header=False, pure_c=False, test=False, exports_sources=False,
         elif template == "bazel_exe":
             from conans.assets.templates.new_v2_bazel import get_bazel_exe_files
             files = get_bazel_exe_files(name, version, package_name)
+        elif template == "autotools_lib":
+            from conans.assets.templates.new_v2_autotools import get_autotools_lib_files
+            files = get_autotools_lib_files(name, version, package_name)
+        elif template == "autotools_exe":
+            from conans.assets.templates.new_v2_autotools import get_autotools_exe_files
+            files = get_autotools_exe_files(name, version, package_name)
         else:
             if not os.path.isabs(template):
                 template = os.path.join(cache.cache_folder, "templates", "command/new", template)


Test patch:
diff --git a/conans/test/functional/toolchains/gnu/autotools/test_ios.py b/conans/test/functional/toolchains/gnu/autotools/test_ios.py
index 8507072d9..272d9571c 100644
--- a/conans/test/functional/toolchains/gnu/autotools/test_ios.py
+++ b/conans/test/functional/toolchains/gnu/autotools/test_ios.py
@@ -76,4 +76,5 @@ def test_ios():
 
     conanbuild = load_toolchain_args(client.current_folder)
     configure_args = conanbuild["configure_args"]
-    assert configure_args == "'--host=aarch64-apple-ios' '--build=x86_64-apple-darwin'"
+    assert configure_args == "'--disable-shared' '--enable-static' '--with-pic' " \
+                             "'--host=aarch64-apple-ios' '--build=x86_64-apple-darwin'"
diff --git a/conans/test/functional/toolchains/gnu/test_v2_autotools_template.py b/conans/test/functional/toolchains/gnu/test_v2_autotools_template.py
new file mode 100644
index 000000000..b7d46889b
--- /dev/null
+++ b/conans/test/functional/toolchains/gnu/test_v2_autotools_template.py
@@ -0,0 +1,62 @@
+import platform
+import re
+import os
+
+import pytest
+
+from conans.model.ref import ConanFileReference, PackageReference
+from conans.test.utils.tools import TestClient
+
+
+@pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Requires Autotools")
+@pytest.mark.tool_autotools()
+def test_autotools_lib_template():
+    client = TestClient(path_with_spaces=False)
+    client.run("new hello/0.1 --template=autotools_lib")
+
+    # Local flow works
+    client.run("install . -if=install")
+    client.run("build . -if=install")
+
+    client.run("export-pkg . hello/0.1@ -if=install")
+    package_id = re.search(r"Packaging to (\S+)", str(client.out)).group(1)
+    pref = PackageReference(ConanFileReference.loads("hello/0.1"), package_id)
+    package_folder = client.cache.package_layout(pref.ref).package(pref)
+    assert os.path.exists(os.path.join(package_folder, "include", "hello.h"))
+
+    # Create works
+    client.run("create .")
+    assert "hello/0.1: Hello World Release!" in client.out
+
+    client.run("create . -s build_type=Debug")
+    assert "hello/0.1: Hello World Debug!" in client.out
+
+    # Create + shared works
+    client.save({}, clean_first=True)
+    client.run("new hello/0.1 --template=autotools_lib")
+    client.run("create . -o hello:shared=True")
+    assert "hello/0.1: Hello World Release!" in client.out
+    if platform.system() == "Darwin":
+        client.run_command("otool -l test_package/build-release/main")
+        assert "libhello.0.dylib" in client.out
+    else:
+        client.run_command("ldd test_package/build-release/main")
+        assert "libhello.so.0" in client.out
+
+
+@pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Requires Autotools")
+@pytest.mark.tool_autotools()
+def test_autotools_exe_template():
+    client = TestClient(path_with_spaces=False)
+    client.run("new greet/0.1 --template=autotools_exe")
+    # Local flow works
+    client.run("install . -if=install")
+    client.run("build . -if=install")
+
+    # Create works
+    client.run("create .")
+    assert "greet/0.1: Hello World Release!" in client.out
+
+    client.run("create . -s build_type=Debug")
+    assert "greet/0.1: Hello World Debug!" in client.out
+
diff --git a/conans/test/unittests/tools/gnu/autotools_test.py b/conans/test/unittests/tools/gnu/autotools_test.py
index ed601630b..50a312f2b 100644
--- a/conans/test/unittests/tools/gnu/autotools_test.py
+++ b/conans/test/unittests/tools/gnu/autotools_test.py
@@ -17,9 +17,10 @@ def test_source_folder_works():
     conanfile.folders.set_base_install(folder)
     sources = "/path/to/sources"
     conanfile.folders.set_base_source(sources)
-    autotools = Autotools(conanfile)
-    autotools.configure(build_script_folder="subfolder")
+    autotools = Autotools(conanfile, build_script_folder="subfolder")
+    autotools.configure()
     assert conanfile.command.replace("\\", "/") == '"/path/to/sources/subfolder/configure" -foo bar'
 
+    autotools = Autotools(conanfile)
     autotools.configure()
     assert conanfile.command.replace("\\", "/") == '"/path/to/sources/configure" -foo bar'