Skip to content

Commit

Permalink
Add fix_apple_shared_install_name tool (#11365)
Browse files Browse the repository at this point in the history
* add tool

* put conditions inside

* use is_apple

* wip

* test

* move to apple
  • Loading branch information
czoido authored Jun 1, 2022
1 parent a98bb36 commit ab6f8be
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 31 deletions.
1 change: 1 addition & 0 deletions conan/tools/apple/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# from conan.tools.apple.apple import apple_sdk_name
# from conan.tools.apple.apple import apple_deployment_target_flag
# from conan.tools.apple.apple import to_apple_arch
from conan.tools.apple.apple import fix_apple_shared_install_name
from conan.tools.apple.xcodedeps import XcodeDeps
from conan.tools.apple.xcodebuild import XcodeBuild
from conan.tools.apple.xcodetoolchain import XcodeToolchain
40 changes: 40 additions & 0 deletions conan/tools/apple/apple.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os

from conans.util.runners import check_output_runner

Expand Down Expand Up @@ -152,3 +153,42 @@ def strip(self):
def libtool(self):
"""path to libtool"""
return self.find('libtool')


def fix_apple_shared_install_name(conanfile):

def _get_install_name(path_to_dylib):
command = "otool -D {}".format(path_to_dylib)
install_name = check_output_runner(command).strip().split(":")[1].strip()
return install_name

def _osx_collect_dylibs(lib_folder):
return [os.path.join(full_folder, f) for f in os.listdir(lib_folder) if f.endswith(".dylib")
and not os.path.islink(os.path.join(lib_folder, f))]

def _fix_install_name(dylib_path, new_name):
command = f"install_name_tool {dylib_path} -id {new_name}"
conanfile.run(command)

def _fix_dep_name(dylib_path, old_name, new_name):
command = f"install_name_tool {dylib_path} -change {old_name} {new_name}"
conanfile.run(command)

substitutions = {}

if is_apple_os(conanfile.settings.get_safe("os")) and conanfile.options.get_safe("shared", False):
libdirs = getattr(conanfile.cpp.package, "libdirs")
for libdir in libdirs:
full_folder = os.path.join(conanfile.package_folder, libdir)
shared_libs = _osx_collect_dylibs(full_folder)
# fix LC_ID_DYLIB in first pass
for shared_lib in shared_libs:
install_name = _get_install_name(shared_lib)
rpath_name = f"@rpath/{os.path.basename(install_name)}"
_fix_install_name(shared_lib, rpath_name)
substitutions[install_name] = rpath_name

# fix dependencies in second pass
for shared_lib in shared_libs:
for old, new in substitutions.items():
_fix_dep_name(shared_lib, old, new)
2 changes: 2 additions & 0 deletions conan/tools/files/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
import six

from conan.tools import CONAN_TOOLCHAIN_ARGS_FILE, CONAN_TOOLCHAIN_ARGS_SECTION
from conan.tools.apple.apple import is_apple_os
from conans.client.downloaders.download import run_downloader
from conans.errors import ConanException
from conans.util.files import rmdir as _internal_rmdir
from conans.util.runners import check_output_runner

if six.PY3: # Remove this IF in develop2
from shutil import which
Expand Down
31 changes: 0 additions & 31 deletions conan/tools/gnu/autotools.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,40 +54,9 @@ def make(self, target=None, args=None):
command = join_arguments([make_program, target, str_args, str_extra_args, jobs])
self._conanfile.run(command)

def _fix_osx_shared_install_name(self):

def _osx_collect_dylibs(lib_folder):
return [f for f in os.listdir(lib_folder) if f.endswith(".dylib")
and not os.path.islink(os.path.join(lib_folder, f))]

def _fix_install_name(lib_name, lib_folder):
command = "install_name_tool -id @rpath/{} {}".format(lib_name, os.path.join(lib_folder,
lib_name))
self._conanfile.run(command)

def _is_modified_install_name(lib_name, full_folder, libdir):
"""
Check that the user did not change the default install_name using the install_name
linker flag in that case we do not touch this field
"""
command = "otool -D {}".format(os.path.join(full_folder, lib_name))
install_path = check_output_runner(command).strip().split(":")[1].strip()
default_path = str(os.path.join("/", libdir, shared_lib))
return False if default_path == install_path else True

libdirs = getattr(self._conanfile.cpp.package, "libdirs")
for libdir in libdirs:
full_folder = os.path.join(self._conanfile.package_folder, libdir)
shared_libs = _osx_collect_dylibs(full_folder)
for shared_lib in shared_libs:
if not _is_modified_install_name(shared_lib, full_folder, libdir):
_fix_install_name(shared_lib, full_folder)

def install(self, args=None):
args = args if args is not None else ["DESTDIR={}".format(self._conanfile.package_folder)]
self.make(target="install", args=args)
if self._conanfile.settings.get_safe("os") == "Macos" and self._conanfile.options.get_safe("shared", False):
self._fix_osx_shared_install_name()

def autoreconf(self, args=None):
args = args or []
Expand Down
2 changes: 2 additions & 0 deletions conans/assets/templates/new_v2_autotools.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from conan import ConanFile
from conan.tools.gnu import AutotoolsToolchain, Autotools
from conan.tools.layout import basic_layout
from conan.tools.apple import fix_apple_shared_install_name
class {package_name}Conan(ConanFile):
Expand Down Expand Up @@ -48,6 +49,7 @@ def build(self):
def package(self):
autotools = Autotools(self)
autotools.install()
fix_apple_shared_install_name(self)
def package_info(self):
self.cpp_info.libs = ["{name}"]
Expand Down
139 changes: 139 additions & 0 deletions conans/test/functional/toolchains/gnu/test_v2_autotools_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,142 @@ def build(self):
client2.run_command("build-release/greet")
assert "Hello World Release!" in client2.out


@pytest.mark.skipif(platform.system() not in ["Darwin"], reason="Only affects apple platforms")
@pytest.mark.tool_autotools()
def test_autotools_fix_shared_libs():
"""
From comments in: https://github.com/conan-io/conan/pull/11365
Case 1:
libopencv_core.3.4.17.dylib
libopencv_core.3.4.dylib (symlink) -> libopencv_core.3.4.17.dylib
libopencv_core.dylib (symlink) -> libopencv_core.3.4.dylib
Install name in libopencv_core.3.4.17.dylib is libopencv_core.3.4.dylib NOT the dylib name
So we have to add the rpath to that.
Case 2:
libopencv_core.dylib
libopencv_imgproc.dylib
libopencv_imgproc.dylib depends on libopencv_core.dylib and declares that dependency not using the
@rpath, we have to make sure that we patch the dependencies in the dylibs using install_name_tool -change
Let's create a Conan package with two libraries: bye and hello (bye depends on hello)
and recreate this whole situation to check that we are correctly fixing the dylibs
"""
client = TestClient(path_with_spaces=False)
client.run("new hello/0.1 --template=autotools_lib")

conanfile = textwrap.dedent("""
import os
from conan import ConanFile
from conan.tools.gnu import AutotoolsToolchain, Autotools
from conan.tools.layout import basic_layout
from conan.tools.apple import fix_apple_shared_install_name
class HelloConan(ConanFile):
name = "hello"
version = "0.1"
settings = "os", "compiler", "build_type", "arch"
options = {"shared": [True, False], "fPIC": [True, False]}
default_options = {"shared": False, "fPIC": True}
exports_sources = "configure.ac", "Makefile.am", "src/*"
def layout(self):
basic_layout(self)
def generate(self):
at_toolchain = AutotoolsToolchain(self)
at_toolchain.generate()
def build(self):
autotools = Autotools(self)
autotools.autoreconf()
autotools.configure()
autotools.make()
def package(self):
autotools = Autotools(self)
autotools.install()
# before fixing the names we try to reproduce the two cases explained
# in the test that dylib name and install name are not the same
self.run("install_name_tool {} -id /lib/libbye.dylib".format(os.path.join(self.package_folder,
"lib", "libbye.0.dylib")))
# also change that in the libbye dependencies
self.run("install_name_tool {} -change /lib/libhello.0.dylib /lib/libhello.dylib".format(os.path.join(self.package_folder,
"lib", "libbye.0.dylib")))
self.run("install_name_tool {} -id /lib/libhello.dylib".format(os.path.join(self.package_folder,
"lib","libhello.0.dylib")))
fix_apple_shared_install_name(self)
def package_info(self):
self.cpp_info.libs = ["hello", "bye"]
""")

bye_cpp = textwrap.dedent("""
#include <iostream>
#include "hello.h"
#include "bye.h"
void bye(){
hello();
std::cout << "Bye, bye!" << std::endl;
}
""")

bye_h = textwrap.dedent("""
#pragma once
void bye();
""")

makefile_am = textwrap.dedent("""
lib_LTLIBRARIES = libhello.la libbye.la
libhello_la_SOURCES = hello.cpp hello.h
libhello_la_HEADERS = hello.h
libhello_ladir = $(includedir)
libbye_la_SOURCES = bye.cpp bye.h
libbye_la_HEADERS = bye.h
libbye_ladir = $(includedir)
libbye_la_LIBADD = libhello.la
""")

test_src = textwrap.dedent("""
#include "bye.h"
int main() { bye(); }
""")

client.save({
"src/makefile.am": makefile_am,
"src/bye.cpp": bye_cpp,
"src/bye.h": bye_h,
"test_package/main.cpp": test_src,
"conanfile.py": conanfile,
})

client.run("create . -o hello:shared=True -tf=None")

package_id = re.search(r"Package (\S+)", str(client.out)).group(1)
package_id = package_id.replace("'", "")
pref = PackageReference(ConanFileReference.loads("hello/0.1"), package_id)
package_folder = client.cache.package_layout(pref.ref).package(pref)

# install name fixed
client.run_command("otool -D {}".format(os.path.join(package_folder, "lib", "libhello.0.dylib")))
assert "@rpath/libhello.dylib" in client.out
client.run_command("otool -D {}".format(os.path.join(package_folder, "lib", "libbye.0.dylib")))
assert "@rpath/libbye.dylib" in client.out

# dependencies fixed
client.run_command("otool -L {}".format(os.path.join(package_folder, "lib", "libbye.0.dylib")))
assert "/lib/libhello.dylib (compatibility version 1.0.0, current version 1.0.0)" not in client.out
assert "/lib/libbye.dylib (compatibility version 1.0.0, current version 1.0.0)" not in client.out
assert "@rpath/libhello.dylib (compatibility version 1.0.0, current version 1.0.0)" in client.out
assert "@rpath/libbye.dylib (compatibility version 1.0.0, current version 1.0.0)" in client.out

client.run("test test_package hello/0.1@ -o hello:shared=True")
assert "Bye, bye!" in client.out

0 comments on commit ab6f8be

Please sign in to comment.