diff --git a/conan/internal/deploy.py b/conan/internal/deploy.py index 1c28895469b..f7fc534bc29 100644 --- a/conan/internal/deploy.py +++ b/conan/internal/deploy.py @@ -102,6 +102,7 @@ def runtime_deploy(graph, output_folder): Deploy all the shared libraries and the executables of the dependencies in a flat directory. It preserves symlinks in case the configuration tools.deployer:symlinks is True. + It preserves the directory structure when having subfolders """ conanfile = graph.root.conanfile output = ConanOutput(scope="runtime_deploy") @@ -136,7 +137,7 @@ def runtime_deploy(graph, output_folder): def _flatten_directory(dep, src_dir, output_dir, symlinks, extension_filter=None): """ - Copy all the files from the source directory in a flat output directory. + Copy all the files from the source directory in a flat output directory, respecting subfolders. An optional string, named extension_filter, can be set to copy only the files with the listed extensions. """ @@ -144,16 +145,20 @@ def _flatten_directory(dep, src_dir, output_dir, symlinks, extension_filter=None output = ConanOutput(scope="runtime_deploy") for src_dirpath, _, src_filenames in os.walk(src_dir, followlinks=symlinks): + rel_path = os.path.relpath(src_dirpath, src_dir) for src_filename in src_filenames: if extension_filter and not any(fnmatch.fnmatch(src_filename, f'*{ext}') for ext in extension_filter): continue src_filepath = os.path.join(src_dirpath, src_filename) - dest_filepath = os.path.join(output_dir, src_filename) + dest_filepath = os.path.join(output_dir, rel_path, src_filename) if not symlinks and os.path.islink(src_filepath): continue + if not os.path.exists(os.path.dirname(dest_filepath)): + os.makedirs(os.path.dirname(dest_filepath)) + if os.path.exists(dest_filepath): if filecmp.cmp(src_filepath, dest_filepath): # Be efficient, do not copy output.verbose(f"{dest_filepath} exists with same contents, skipping copy") diff --git a/test/functional/command/test_install_deploy.py b/test/functional/command/test_install_deploy.py index faaeb98cf7b..87ce8c84171 100644 --- a/test/functional/command/test_install_deploy.py +++ b/test/functional/command/test_install_deploy.py @@ -508,8 +508,9 @@ def package_info(self): c.run("install --requires=pkga/1.0 --requires=pkgb/1.0 --deployer=runtime_deploy " "--deployer-folder=myruntime -vvv") - expected = sorted(["pkga.so", "pkgb.so", "pkga.dll"]) - assert sorted(os.listdir(os.path.join(c.current_folder, "myruntime"))) == expected + assert sorted(os.listdir(os.path.join(c.current_folder, "myruntime"))) == sorted(["bin", "lib"]) + assert sorted(os.listdir(os.path.join(c.current_folder, "myruntime", "lib"))) == sorted(['pkga.so', 'pkgb.so']) + assert sorted(os.listdir(os.path.join(c.current_folder, "myruntime", "bin"))) == sorted(['pkga.dll']) @pytest.mark.parametrize("symlink, expected", [(True, ["libfoo.so.0.1.0", "libfoo.so.0", "libfoo.so"]), @@ -553,6 +554,68 @@ def package(self): assert not os.path.islink(lib) +def test_runtime_deploy_subfolder(): + """ The deployer runtime_deploy should preserve subfolder structure when deploying shared libraries + """ + c = TestClient() + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.files import copy + import os + class Pkg(ConanFile): + package_type = "shared-library" + def package(self): + copy(self, "*.so*", src=self.build_folder, dst=self.package_folder, keep_path=True) + """) + c.save({"foo/conanfile.py": conanfile, + "foo/lib/libfoo.so": "", + "foo/lib/subfolder/libbar.so": "", + "foo/lib/subfolder/subsubfolder/libqux.so": "",}) + c.run("export-pkg foo/ --name=foo --version=0.1.0") + c.run(f"install --requires=foo/0.1.0 --deployer=runtime_deploy --deployer-folder=output") + + assert sorted(os.listdir(os.path.join(c.current_folder, "output"))) == ["libfoo.so", "subfolder"] + assert sorted(os.listdir(os.path.join(c.current_folder, "output", "subfolder"))) == ["libbar.so", "subsubfolder"] + assert sorted(os.listdir(os.path.join(c.current_folder, "output", "subfolder", "subsubfolder"))) == ["libqux.so"] + + +def test_runtime_deploy_subfolder_symlink(): + """ The deployer runtime_deploy should preserve subfolder structure when deploying shared + libraries with symlinks + """ + c = TestClient() + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.files import copy, chdir, mkdir + import os + class Pkg(ConanFile): + package_type = "shared-library" + def package(self): + copy(self, "*.so*", src=self.build_folder, dst=self.package_folder) + with chdir(self, os.path.join(self.package_folder, "lib")): + os.symlink(src="subfolder/libfoo.so.1.0", dst="libfoo.so.1") + os.symlink(src="libfoo.so.1", dst="libfoo.so") + """) + c.save({"foo/conanfile.py": conanfile, + "foo/lib/subfolder/libfoo.so.1.0": "",}) + c.run("export-pkg foo/ --name=foo --version=0.1.0") + c.run(f"install --requires=foo/0.1.0 --deployer=runtime_deploy --deployer-folder=output -c:a tools.deployer:symlinks=True") + + assert os.listdir(os.path.join(c.current_folder, "output", "subfolder")) == ["libfoo.so.1.0"] + # INFO: This test requires in Windows to have symlinks enabled, otherwise it will fail + if platform.system() != "Windows": + assert sorted(os.listdir(os.path.join(c.current_folder, "output"))) == ["libfoo.so", "libfoo.so.1" , "subfolder"] + link_so_0 = os.path.join(c.current_folder, "output", "libfoo.so.1") + link_so = os.path.join(c.current_folder, "output", "libfoo.so") + lib = os.path.join(c.current_folder, "output", "subfolder", "libfoo.so.1.0") + assert os.path.islink(link_so_0) + assert os.path.islink(link_so) + assert not os.path.isabs(os.readlink(link_so_0)) + assert not os.path.isabs(os.readlink(os.path.join(link_so))) + assert os.path.realpath(link_so) == os.path.realpath(link_so_0) + assert os.path.realpath(link_so_0) == os.path.realpath(lib) + + def test_deployer_errors(): c = TestClient() c.save({"conanfile.txt": "",