Repository: conan-io/conan
Pull request #16486 (2024-06-16 11:44:07)

Qbs: pass provider args if QbsDeps was run
Now Qbs helper passes required arguments for the "conan" provider to qbs if QbsDeps was run.

Changelog: (Feature): Qbs helper now invokes Conan provider automatically
Docs: Omit

- [ ] 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/develop2/.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.



Patch:
diff --git a/conan/cli/commands/list.py b/conan/cli/commands/list.py
index a905c2acd..ed700fe5e 100644
--- a/conan/cli/commands/list.py
+++ b/conan/cli/commands/list.py
@@ -184,8 +184,9 @@ def prepare_pkglist_compact(pkglist):
             pkg_common_options = compute_common_options(prefs)
             pkg_common_options = pkg_common_options if len(pkg_common_options) > 4 else None
             for pref, pref_contents in prefs.items():
-                pref_info = pref_contents.pop("info")
-                pref_contents.update(compact_format_info(pref_info, pkg_common_options))
+                pref_info = pref_contents.pop("info", None)
+                if pref_info is not None:
+                    pref_contents.update(compact_format_info(pref_info, pkg_common_options))
                 diff_info = pref_contents.pop("diff", None)
                 if diff_info is not None:
                     pref_contents["diff"] = compact_diff(diff_info)
diff --git a/conan/tools/qbs/qbs.py b/conan/tools/qbs/qbs.py
index b6159e2bc..9ef5e8338 100644
--- a/conan/tools/qbs/qbs.py
+++ b/conan/tools/qbs/qbs.py
@@ -55,6 +55,10 @@ class Qbs(object):
         if self.profile:
             args.append('profile:%s' % self.profile)
 
+        generators_folder = self._conanfile.generators_folder
+        if os.path.exists(os.path.join(generators_folder, 'conan-qbs-deps')):
+            args.append('moduleProviders.conan.installDirectory:' + generators_folder)
+
         for name in self._configuration:
             config = self._configuration[name]
             args.extend(_configuration_dict_to_commandlist(name, config))
diff --git a/conans/util/files.py b/conans/util/files.py
index 9003077ed..d83395f11 100644
--- a/conans/util/files.py
+++ b/conans/util/files.py
@@ -306,12 +306,15 @@ def gather_files(folder):
     file_dict = {}
     symlinked_folders = {}
     for root, dirs, files in os.walk(folder):
+        if root != folder and not dirs and not files:  # empty folder
+            rel_path = root[len(folder) + 1:].replace("\\", "/")
+            symlinked_folders[rel_path] = root
+            continue
         for d in dirs:
             abs_path = os.path.join(root, d)
             if os.path.islink(abs_path):
                 rel_path = abs_path[len(folder) + 1:].replace("\\", "/")
                 symlinked_folders[rel_path] = abs_path
-                continue
         for f in files:
             if f == ".DS_Store":
                 continue


Test patch:
diff --git a/test/functional/toolchains/qbs/test_qbsdeps.py b/test/functional/toolchains/qbs/test_qbsdeps.py
index 3dde907f5..8302397be 100644
--- a/test/functional/toolchains/qbs/test_qbsdeps.py
+++ b/test/functional/toolchains/qbs/test_qbsdeps.py
@@ -92,3 +92,44 @@ def test_qbsdeps_with_test_requires_lib():
     }, clean_first=True)
     client.run("install .")
     client.run_command("qbs build moduleProviders.conan.installDirectory:.")
+
+
+@pytest.mark.tool("qbs")
+def test_qbsdeps_with_qbs_toolchain():
+    client = TestClient()
+    with client.chdir("lib"):
+        client.run("new qbs_lib -d name=hello -d version=1.0")
+        client.run("create . -tf=\"\"")
+
+    conanfile = textwrap.dedent('''
+        from conan import ConanFile
+        from conan.tools.qbs import Qbs
+
+        class Recipe(ConanFile):
+            name = "app"
+            version = "1.2"
+            exports_sources = "*.cpp", "*.h", "*.qbs"
+            settings = "os", "compiler", "build_type", "arch"
+            generators = "QbsDeps"
+
+            def requirements(self):
+                self.requires("hello/1.0")
+
+            def layout(self):
+                self.folders.generators = "generators"
+
+            def build(self):
+                qbs = Qbs(self)
+                qbs.resolve()
+                qbs.build()
+
+            def install(self):
+                qbs = Qbs(self)
+                qbs.install()
+        ''')
+    client.save({
+        "conanfile.py": conanfile,
+        "main.cpp": gen_function_cpp(name="main", includes=["hello"], calls=["hello"]),
+        "app.qbs": gen_qbs_application(files=["main.cpp"], deps=["hello"]),
+    }, clean_first=True)
+    client.run("create .")
diff --git a/test/integration/command_v2/list_test.py b/test/integration/command_v2/list_test.py
index 8da89272e..b73c9742e 100644
--- a/test/integration/command_v2/list_test.py
+++ b/test/integration/command_v2/list_test.py
@@ -812,6 +812,24 @@ class TestListCompact:
 
         assert expected == expected_output
 
+    @pytest.mark.parametrize("pattern", [
+        "pkg/*",
+        "pkg/1.0",
+        "pkg/1.0#*",
+        "pkg/1.0#*:*",
+        "pkg/1.0#*:*#*",
+        "pkg/1.0#a69a86bbd19ae2ef7eedc64ae645c531:*",
+        "pkg/1.0#a69a86bbd19ae2ef7eedc64ae645c531:*",
+        "pkg/1.0#a69a86bbd19ae2ef7eedc64ae645c531:*#*",
+        "pkg/1.0#a69a86bbd19ae2ef7eedc64ae645c531:da39a3ee5e6b4b0d3255bfef95601890afd80709#*",
+        "pkg/1.0#a69a86bbd19ae2ef7eedc64ae645c531:da39a3ee5e6b4b0d3255bfef95601890afd80709#0ba8627bd47edc3a501e8f0eb9a79e5e"
+    ])
+    def test_list_compact_patterns(self, pattern):
+        c = TestClient(light=True)
+        c.save({"pkg/conanfile.py": GenConanfile("pkg", "1.0")})
+        c.run("create pkg")
+        c.run(f"list {pattern} --format=compact")
+
 
 class TestListBinaryFilter:
 
diff --git a/test/integration/test_package_python_files.py b/test/integration/test_package_python_files.py
index 3b76cecd4..2c2032143 100644
--- a/test/integration/test_package_python_files.py
+++ b/test/integration/test_package_python_files.py
@@ -65,3 +65,33 @@ def test_package_python_files():
     assert "myfile.pyc" in manifest
     assert "myfile.pyo" in manifest
     assert ".DS_Store" not in manifest
+
+
+def test_package_empty_folders():
+    c = TestClient(default_server_user=True)
+
+    conanfile = textwrap.dedent("""
+        import os
+        from conan import ConanFile
+        class Pkg(ConanFile):
+            def package(self):
+                os.mkdir(os.path.join(self.package_folder, "empty_folder"))
+        """)
+    c.save({"conanfile.py": conanfile})
+    c.run("create . --name=pkg --version=0.1")
+    pkg_folder = c.created_layout().package()
+    assert os.path.isdir(os.path.join(pkg_folder, "empty_folder"))
+    manifest = load(os.path.join(pkg_folder, "conanmanifest.txt"))
+    assert "empty_folder" not in manifest
+
+    c.run("upload * -r=default --confirm")
+    c.run("remove * -c")
+    c.run("download pkg/0.1#*:* -r default")
+
+    # The download will be in a different pkg folder now.
+    ref = RecipeReference.loads("pkg/0.1")
+    pref = c.get_latest_package_reference(ref, NO_SETTINGS_PACKAGE_ID)
+    pkg_folder = c.get_latest_pkg_layout(pref).package()
+    assert os.path.isdir(os.path.join(pkg_folder, "empty_folder"))
+    manifest = load(os.path.join(pkg_folder, "conanmanifest.txt"))
+    assert "empty_folder" not in manifest