Skip to content

Commit 1ac69d3

Browse files
[fix] The deployer runtime_deploy should preserve symlinks (#17824)
* 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>
1 parent c27b03f commit 1ac69d3

File tree

2 files changed

+53
-3
lines changed

2 files changed

+53
-3
lines changed

conan/internal/deploy.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import filecmp
22
import os
33
import shutil
4+
import fnmatch
45

56
from conan.internal.cache.home_paths import HomePaths
67
from conan.api.output import ConanOutput
@@ -99,6 +100,8 @@ def full_deploy(graph, output_folder):
99100
def runtime_deploy(graph, output_folder):
100101
"""
101102
Deploy all the shared libraries and the executables of the dependencies in a flat directory.
103+
104+
It preserves symlinks in case the configuration tools.deployer:symlinks is True.
102105
"""
103106
conanfile = graph.root.conanfile
104107
output = ConanOutput(scope="runtime_deploy")
@@ -125,7 +128,7 @@ def runtime_deploy(graph, output_folder):
125128
if not os.path.isdir(libdir):
126129
output.warning(f"{dep.ref} {libdir} does not exist")
127130
continue
128-
count += _flatten_directory(dep, libdir, output_folder, symlinks, [".dylib", ".so"])
131+
count += _flatten_directory(dep, libdir, output_folder, symlinks, [".dylib*", ".so*"])
129132

130133
output.info(f"Copied {count} files from {dep.ref}")
131134
conanfile.output.success(f"Runtime deployed to folder: {output_folder}")
@@ -142,11 +145,15 @@ def _flatten_directory(dep, src_dir, output_dir, symlinks, extension_filter=None
142145
output = ConanOutput(scope="runtime_deploy")
143146
for src_dirpath, _, src_filenames in os.walk(src_dir, followlinks=symlinks):
144147
for src_filename in src_filenames:
145-
if extension_filter and not any(src_filename.endswith(ext) for ext in extension_filter):
148+
if extension_filter and not any(fnmatch.fnmatch(src_filename, f'*{ext}') for ext in extension_filter):
146149
continue
147150

148151
src_filepath = os.path.join(src_dirpath, src_filename)
149152
dest_filepath = os.path.join(output_dir, src_filename)
153+
154+
if not symlinks and os.path.islink(src_filepath):
155+
continue
156+
150157
if os.path.exists(dest_filepath):
151158
if filecmp.cmp(src_filepath, dest_filepath): # Be efficient, do not copy
152159
output.verbose(f"{dest_filepath} exists with same contents, skipping copy")
@@ -156,7 +163,9 @@ def _flatten_directory(dep, src_dir, output_dir, symlinks, extension_filter=None
156163

157164
try:
158165
file_count += 1
159-
shutil.copy2(src_filepath, dest_filepath, follow_symlinks=symlinks)
166+
# INFO: When follow_symlinks is false, and src is a symbolic link, it tries to
167+
# copy all metadata from the src symbolic link to the newly created dst link
168+
shutil.copy2(src_filepath, dest_filepath, follow_symlinks=not symlinks)
160169
output.verbose(f"Copied {src_filepath} into {output_dir}")
161170
except Exception as e:
162171
if "WinError 1314" in str(e):

test/functional/command/test_install_deploy.py

+41
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,47 @@ def package_info(self):
511511
expected = sorted(["pkga.so", "pkgb.so", "pkga.dll"])
512512
assert sorted(os.listdir(os.path.join(c.current_folder, "myruntime"))) == expected
513513

514+
@pytest.mark.parametrize("symlink, expected",
515+
[(True, ["libfoo.so.0.1.0", "libfoo.so.0", "libfoo.so"]),
516+
(False, ["libfoo.so.0.1.0",])])
517+
def test_runtime_deploy_symlinks(symlink, expected):
518+
""" The deployer runtime_deploy should preserve symlinks when deploying shared libraries
519+
"""
520+
c = TestClient()
521+
conanfile = textwrap.dedent("""
522+
from conan import ConanFile
523+
from conan.tools.files import copy, chdir
524+
import os
525+
class Pkg(ConanFile):
526+
package_type = "shared-library"
527+
def package(self):
528+
copy(self, "*.so*", src=self.build_folder, dst=self.package_folder)
529+
with chdir(self, os.path.join(self.package_folder, "lib")):
530+
os.symlink(src="libfoo.so.0.1.0", dst="libfoo.so.0")
531+
os.symlink(src="libfoo.so.0", dst="libfoo.so")
532+
""")
533+
c.save({"foo/conanfile.py": conanfile,
534+
"foo/lib/libfoo.so.0.1.0": "",})
535+
c.run("export-pkg foo/ --name=foo --version=0.1.0")
536+
c.run(f"install --requires=foo/0.1.0 --deployer=runtime_deploy --deployer-folder=output -c:a tools.deployer:symlinks={symlink}")
537+
538+
sorted_expected = sorted(expected)
539+
assert sorted(os.listdir(os.path.join(c.current_folder, "output"))) == sorted_expected
540+
link_so_0 = os.path.join(c.current_folder, "output", "libfoo.so.0")
541+
link_so = os.path.join(c.current_folder, "output", "libfoo.so")
542+
lib = os.path.join(c.current_folder, "output", "libfoo.so.0.1.0")
543+
# INFO: This test requires in Windows to have symlinks enabled, otherwise it will fail
544+
if symlink and platform.system() != "Windows":
545+
assert os.path.islink(link_so_0)
546+
assert os.path.islink(link_so)
547+
assert not os.path.isabs(os.readlink(link_so_0))
548+
assert not os.path.isabs(os.readlink(os.path.join(link_so)))
549+
assert os.path.realpath(link_so) == os.path.realpath(link_so_0)
550+
assert os.path.realpath(link_so_0) == os.path.realpath(lib)
551+
assert not os.path.islink(lib)
552+
else:
553+
assert not os.path.islink(lib)
554+
514555

515556
def test_deployer_errors():
516557
c = TestClient()

0 commit comments

Comments
 (0)