Repository: conan-io/conan
Pull request #15741 (2024-02-22 16:22:57)

add --output-folder to export-pkg
Changelog: Feature: Add ``--output-folder`` to ``export-pkg`` command to be used when ``layout()`` is declared.
Docs: https://github.com/conan-io/docs/pull/3661

Close https://github.com/conan-io/conan/issues/15732



Patch:
diff --git a/conan/tools/gnu/autotoolstoolchain.py b/conan/tools/gnu/autotoolstoolchain.py
index b870bd5b0..cf4af9063 100644
--- a/conan/tools/gnu/autotoolstoolchain.py
+++ b/conan/tools/gnu/autotoolstoolchain.py
@@ -1,6 +1,7 @@
 from conan.tools._check_build_profile import check_using_build_profile
 from conan.tools._compilers import architecture_flag, build_type_flags, cppstd_flag, \
     build_type_link_flags, libcxx_flags
+from conan.tools.apple import is_apple_os
 from conan.tools.apple.apple import apple_min_version_flag, to_apple_arch, \
     apple_sdk_path
 from conan.tools.build.cross_building import cross_building, get_cross_building_settings
@@ -59,7 +60,7 @@ class AutotoolsToolchain:
             # Build triplet
             self._build = _get_gnu_triplet(os_build, arch_build, compiler=compiler)
             # Apple Stuff
-            if os_build == "Macos":
+            if os_build == "Macos" and is_apple_os(self._conanfile):
                 sdk_path = apple_sdk_path(conanfile)
                 apple_arch = to_apple_arch(self._conanfile)
                 # https://man.archlinux.org/man/clang.1.en#Target_Selection_Options
diff --git a/conan/tools/meson/meson.py b/conan/tools/meson/meson.py
index e16326afa..19515272e 100644
--- a/conan/tools/meson/meson.py
+++ b/conan/tools/meson/meson.py
@@ -1,8 +1,8 @@
 import os
 
 from conan.tools.build import build_jobs
-from conan.tools.meson.toolchain import MesonToolchain
 from conan.tools.meson.mesondeps import MesonDeps
+from conan.tools.meson.toolchain import MesonToolchain
 
 
 class Meson(object):
@@ -10,6 +10,9 @@ class Meson(object):
         self._conanfile = conanfile
 
     def configure(self, reconfigure=False):
+        if reconfigure:
+            self._conanfile.output.warning("reconfigure param has been deprecated."
+                                           " Removing in Conan 2.x.")
         source_folder = self._conanfile.source_folder
         build_folder = self._conanfile.build_folder
         cmd = "meson setup"
@@ -35,10 +38,8 @@ class Meson(object):
 
         cmd += "".join([f'{cmd_param} "{meson_option}"' for meson_option in meson_filenames])
         cmd += ' "{}" "{}"'.format(build_folder, source_folder)
-        if self._conanfile.package_folder:
-            cmd += ' -Dprefix="{}"'.format(self._conanfile.package_folder)
-        if reconfigure:
-            cmd += ' --reconfigure'
+        # Issue related: https://github.com/mesonbuild/meson/issues/12880
+        cmd += ' --prefix=/'  # this must be an absolute path, otherwise, meson complains
         self._conanfile.output.info("Meson configure cmd: {}".format(cmd))
         self._conanfile.run(cmd)
 
@@ -54,9 +55,10 @@ class Meson(object):
         self._conanfile.run(cmd)
 
     def install(self):
-        self.configure(reconfigure=True)  # To re-do the destination package-folder
-        meson_build_folder = self._conanfile.build_folder
-        cmd = 'meson install -C "{}"'.format(meson_build_folder)
+        meson_build_folder = self._conanfile.build_folder.replace("\\", "/")
+        meson_package_folder = self._conanfile.package_folder.replace("\\", "/")
+        # Assuming meson >= 0.57.0
+        cmd = f'meson install -C "{meson_build_folder}" --destdir "{meson_package_folder}"'
         self._conanfile.run(cmd)
 
     def test(self):
diff --git a/conans/client/cmd/export_pkg.py b/conans/client/cmd/export_pkg.py
index cc40c2d9b..de57c605a 100644
--- a/conans/client/cmd/export_pkg.py
+++ b/conans/client/cmd/export_pkg.py
@@ -10,7 +10,7 @@ from conans.model.ref import PackageReference
 
 
 def export_pkg(app, recorder, full_ref, source_folder, build_folder, package_folder, install_folder,
-               graph_info, force, remotes, source_conanfile_path):
+               graph_info, force, remotes, source_conanfile_path, output_folder):
     ref = full_ref.copy_clear_rev()
     cache, output, hook_manager = app.cache, app.out, app.hook_manager
     graph_manager = app.graph_manager
@@ -59,7 +59,7 @@ def export_pkg(app, recorder, full_ref, source_folder, build_folder, package_fol
     conanfile.develop = True
     if hasattr(conanfile, "layout"):
         conanfile_folder = os.path.dirname(source_conanfile_path)
-        conanfile.folders.set_base_folders(conanfile_folder, output_folder=None)
+        conanfile.folders.set_base_folders(conanfile_folder, output_folder=output_folder)
         conanfile.folders.set_base_package(dest_package_folder)
     else:
         conanfile.folders.set_base_build(build_folder)
diff --git a/conans/client/command.py b/conans/client/command.py
index 0a7036104..76d1eddd8 100644
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -143,6 +143,7 @@ class Command(object):
         Creates a new package recipe template with a 'conanfile.py' and optionally,
         'test_package' testing files.
         """
+        self._warn_conan_version()
         parser = argparse.ArgumentParser(description=self.new.__doc__,
                                          prog="conan new",
                                          formatter_class=SmartFormatter)
@@ -280,6 +281,7 @@ class Command(object):
         neither definition of package() or package_info() methods. The package
         to be tested must exist in the local cache or any configured remote.
         """
+        self._warn_conan_version()
         parser = argparse.ArgumentParser(description=self.test.__doc__,
                                          prog="conan test",
                                          formatter_class=SmartFormatter)
@@ -396,6 +398,7 @@ class Command(object):
                                       profile_build=profile_build,
                                       is_build_require=args.build_require,
                                       require_overrides=args.require_override)
+            self._warn_revisions()
         except ConanException as exc:
             info = exc.info
             raise
@@ -414,7 +417,7 @@ class Command(object):
         is specified, it will download all packages from the specified remote.
         If no remote is specified, it will use the default remote.
         """
-
+        self._warn_conan_version()
         parser = argparse.ArgumentParser(description=self.download.__doc__,
                                          prog="conan download",
                                          formatter_class=SmartFormatter)
@@ -567,6 +570,8 @@ class Command(object):
                                                      is_build_require=args.build_require,
                                                      require_overrides=args.require_override)
 
+            self._warn_revisions()
+
         except ConanException as exc:
             info = exc.info
             raise
@@ -672,7 +677,6 @@ class Command(object):
         It can be used with a recipe or a reference for any existing package in
         your local cache.
         """
-
         info_only_options = ["id", "build_id", "remote", "url", "license", "requires", "update",
                              "required", "date", "author", "description", "provides", "deprecated",
                              "None"]
@@ -825,6 +829,7 @@ class Command(object):
 
         Usually downloads and uncompresses the package sources.
         """
+        self._warn_conan_version()
         parser = argparse.ArgumentParser(description=self.source.__doc__,
                                          prog="conan source",
                                          formatter_class=SmartFormatter)
@@ -1000,7 +1005,7 @@ class Command(object):
         will execute package() method over '--source-folder' and '--build-folder' to create
         the binary package.
         """
-
+        self._warn_conan_version()
         parser = argparse.ArgumentParser(description=self.export_pkg.__doc__,
                                          prog="conan export-pkg",
                                          formatter_class=SmartFormatter)
@@ -1021,6 +1026,8 @@ class Command(object):
                             help="folder containing a locally created package. If a value is given,"
                                  " it won't call the recipe 'package()' method, and will run a copy"
                                  " of the provided folder.")
+        parser.add_argument("-of", "--output-folder", action=OnceArgument,
+                            help="To be used with 'layout()' recipes, useless otherwise")
         parser.add_argument("-sf", "--source-folder", action=OnceArgument, help=_SOURCE_FOLDER_HELP)
         parser.add_argument("-j", "--json", default=None, action=OnceArgument,
                             help='Path to a json file where the install information will be '
@@ -1057,6 +1064,7 @@ class Command(object):
                                           build_folder=args.build_folder,
                                           package_folder=args.package_folder,
                                           install_folder=args.install_folder,
+                                          output_folder=args.output_folder,
                                           profile_names=args.profile_host,
                                           env=args.env_host,
                                           settings=args.settings_host,
@@ -1084,6 +1092,7 @@ class Command(object):
         it. Once the recipe is in the local cache it can be shared and reused
         with any remote with the 'conan upload' command.
         """
+        self._warn_conan_version()
         parser = argparse.ArgumentParser(description=self.export.__doc__,
                                          prog="conan export",
                                          formatter_class=SmartFormatter)
@@ -1209,6 +1218,8 @@ class Command(object):
         Useful to promote packages (e.g. from "beta" to "stable") or transfer
         them from one user to another.
         """
+        self._out.warning("Command 'conan copy' is highly discouraged and it has been removed"
+                          " in Conan 2")
         parser = argparse.ArgumentParser(description=self.copy.__doc__,
                                          prog="conan copy",
                                          formatter_class=SmartFormatter)
@@ -2181,10 +2192,13 @@ class Command(object):
             self._out.writeln("*"*width, front=Color.BRIGHT_RED)
 
     def _warn_conan_version(self):
-        width = 70
-        self._out.writeln(textwrap.fill("Conan 1 is on a deprecation path, "
-                                        "please consider migrating to Conan 2", width),
-                          front=Color.BRIGHT_YELLOW)
+        self._out.warning("*** Conan 1 is legacy and on a deprecation path ***")
+        self._out.warning("*** Please upgrade to Conan 2 ***")
+
+    def _warn_revisions(self):
+        if self._conan.app and not self._conan.app.config.revisions_enabled:
+            self._conan.app.out.warning("Revisions are disabled. Using Conan without revisions "
+                                        "enabled is deprecated")
 
     def run(self, *args):
         """HIDDEN: entry point for executing commands, dispatcher to class
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
index 0b42b3952..6a3fa3e43 100644
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -407,7 +407,7 @@ class ConanAPIV1(object):
                    package_folder=None, install_folder=None, profile_names=None, settings=None,
                    options=None, env=None, force=False, user=None, version=None, cwd=None,
                    lockfile=None, lockfile_out=None, ignore_dirty=False, profile_build=None,
-                   conf=None):
+                   conf=None, output_folder=None):
         profile_host = ProfileData(profiles=profile_names, settings=settings, options=options,
                                    env=env, conf=conf)
         remotes = self.app.load_remotes()
@@ -423,6 +423,7 @@ class ConanAPIV1(object):
                                          "and source folders")
                 package_folder = _make_abs_path(package_folder, cwd)
 
+            output_folder = _make_abs_path(output_folder, cwd) if output_folder else None
             build_folder = _make_abs_path(build_folder, cwd)
             if install_folder:
                 install_folder = _make_abs_path(install_folder, cwd)
@@ -453,7 +454,8 @@ class ConanAPIV1(object):
             export_pkg(self.app, recorder, new_ref, source_folder=source_folder,
                        build_folder=build_folder, package_folder=package_folder,
                        install_folder=install_folder, graph_info=graph_info, force=force,
-                       remotes=remotes, source_conanfile_path=conanfile_path)
+                       remotes=remotes, source_conanfile_path=conanfile_path,
+                       output_folder=output_folder)
             if lockfile_out:
                 lockfile_out = _make_abs_path(lockfile_out, cwd)
                 graph_lock_file = GraphLockFile(graph_info.profile_host, graph_info.profile_build,


Test patch:
diff --git a/conans/test/functional/layout/test_local_commands.py b/conans/test/functional/layout/test_local_commands.py
index 0c07d5cdd..be937b615 100644
--- a/conans/test/functional/layout/test_local_commands.py
+++ b/conans/test/functional/layout/test_local_commands.py
@@ -451,3 +451,25 @@ def test_local_folders_without_layout():
     client.run("install .")
     assert "conanfile.py (test/1.2.3): generate sf: {}".format(client.current_folder) in client.out
     assert "conanfile.py (test/1.2.3): generate bf: {}".format(client.current_folder) in client.out
+
+
+class TestLocalFlowExportPkg:
+    @pytest.mark.parametrize("with_layout", [True, False])
+    def test_local_flow_layout(self, with_layout):
+        c = TestClient()
+        c.run("new hello/0.1 -m=cmake_lib")
+        if not with_layout:
+            conanfile = c.load("conanfile.py")
+            conanfile = conanfile.replace("def layout(", "def layout2(")
+            c.save({"conanfile.py": conanfile})
+
+        c.run("install . -of=build -if=build")
+        c.run("build . -bf=build -if=build")
+        c.run("export-pkg . -bf=build -of=build")
+        package_id = re.search(r"hello/0.1: Package '(\S+)' created", str(c.out)).group(1)
+        ref = ConanFileReference.loads("hello/0.1@")
+        pref = PackageReference(ref, package_id)
+        pf = c.cache.package_layout(ref).package(pref)
+        assert os.path.exists(os.path.join(pf, "include", "hello.h"))
+        libname = "hello.lib" if platform.system() == "Windows" else "libhello.a"
+        assert os.path.exists(os.path.join(pf, "lib", libname))
diff --git a/conans/test/unittests/client/toolchain/autotools/autotools_toolchain_test.py b/conans/test/unittests/client/toolchain/autotools/autotools_toolchain_test.py
index aedd2fdf9..cd64d20fe 100644
--- a/conans/test/unittests/client/toolchain/autotools/autotools_toolchain_test.py
+++ b/conans/test/unittests/client/toolchain/autotools/autotools_toolchain_test.py
@@ -315,6 +315,19 @@ def test_apple_min_os_flag():
     assert expected in env["LDFLAGS"]
 
 
+def test_crossbuild_from_macos_to_non_apple_os():
+    """Check we are not adding Apple-specific flags
+        when the os_build is Macos, but we are targetting
+        a non-Apple OS (e.g. Linux, Android, QNX)"""
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"os": "Android", "arch": "armv8"})
+    conanfile.settings_build = MockSettings({"os": "Macos", "arch": "armv8"})
+    be = AutotoolsToolchain(conanfile)
+    assert be.apple_min_version_flag == ''
+    assert be.apple_arch_flag is None
+    assert be.apple_isysroot_flag is None
+
+
 def test_apple_isysrootflag():
     """Even when no cross building it is adjusted because it could target a Mac version"""
     conanfile = ConanFileMock()