Skip to content

Commit

Permalink
[fix] The deployer runtime_deploy should preserve symlinks (#17824)
Browse files Browse the repository at this point in the history
* Validate runtime deploy with symlinks

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* Preserve previous method structure

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* Use fnmatch for preserve symlinks

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* Do not copy symlinks when not requested

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* Add comment about testing in Windows

Co-authored-by: James <memsharded@gmail.com>

* Validate links

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* Skip symlink check on Windows

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* Validate when not using synlink

Signed-off-by: Uilian Ries <uilianries@gmail.com>

---------

Signed-off-by: Uilian Ries <uilianries@gmail.com>
Co-authored-by: James <memsharded@gmail.com>
  • Loading branch information
uilianries and memsharded authored Feb 25, 2025
1 parent c27b03f commit 1ac69d3
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 3 deletions.
15 changes: 12 additions & 3 deletions conan/internal/deploy.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import filecmp
import os
import shutil
import fnmatch

from conan.internal.cache.home_paths import HomePaths
from conan.api.output import ConanOutput
Expand Down Expand Up @@ -99,6 +100,8 @@ def full_deploy(graph, output_folder):
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.
"""
conanfile = graph.root.conanfile
output = ConanOutput(scope="runtime_deploy")
Expand All @@ -125,7 +128,7 @@ def runtime_deploy(graph, output_folder):
if not os.path.isdir(libdir):
output.warning(f"{dep.ref} {libdir} does not exist")
continue
count += _flatten_directory(dep, libdir, output_folder, symlinks, [".dylib", ".so"])
count += _flatten_directory(dep, libdir, output_folder, symlinks, [".dylib*", ".so*"])

output.info(f"Copied {count} files from {dep.ref}")
conanfile.output.success(f"Runtime deployed to folder: {output_folder}")
Expand All @@ -142,11 +145,15 @@ 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):
for src_filename in src_filenames:
if extension_filter and not any(src_filename.endswith(ext) for ext in extension_filter):
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)

if not symlinks and os.path.islink(src_filepath):
continue

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")
Expand All @@ -156,7 +163,9 @@ def _flatten_directory(dep, src_dir, output_dir, symlinks, extension_filter=None

try:
file_count += 1
shutil.copy2(src_filepath, dest_filepath, follow_symlinks=symlinks)
# INFO: When follow_symlinks is false, and src is a symbolic link, it tries to
# copy all metadata from the src symbolic link to the newly created dst link
shutil.copy2(src_filepath, dest_filepath, follow_symlinks=not symlinks)
output.verbose(f"Copied {src_filepath} into {output_dir}")
except Exception as e:
if "WinError 1314" in str(e):
Expand Down
41 changes: 41 additions & 0 deletions test/functional/command/test_install_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,47 @@ def package_info(self):
expected = sorted(["pkga.so", "pkgb.so", "pkga.dll"])
assert sorted(os.listdir(os.path.join(c.current_folder, "myruntime"))) == expected

@pytest.mark.parametrize("symlink, expected",
[(True, ["libfoo.so.0.1.0", "libfoo.so.0", "libfoo.so"]),
(False, ["libfoo.so.0.1.0",])])
def test_runtime_deploy_symlinks(symlink, expected):
""" The deployer runtime_deploy should preserve symlinks when deploying shared libraries
"""
c = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile
from conan.tools.files import copy, chdir
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="libfoo.so.0.1.0", dst="libfoo.so.0")
os.symlink(src="libfoo.so.0", dst="libfoo.so")
""")
c.save({"foo/conanfile.py": conanfile,
"foo/lib/libfoo.so.0.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={symlink}")

sorted_expected = sorted(expected)
assert sorted(os.listdir(os.path.join(c.current_folder, "output"))) == sorted_expected
link_so_0 = os.path.join(c.current_folder, "output", "libfoo.so.0")
link_so = os.path.join(c.current_folder, "output", "libfoo.so")
lib = os.path.join(c.current_folder, "output", "libfoo.so.0.1.0")
# INFO: This test requires in Windows to have symlinks enabled, otherwise it will fail
if symlink and platform.system() != "Windows":
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)
assert not os.path.islink(lib)
else:
assert not os.path.islink(lib)


def test_deployer_errors():
c = TestClient()
Expand Down

0 comments on commit 1ac69d3

Please sign in to comment.