Repository: conan-io/conan
Pull request #13034 (2023-02-02 16:10:24)

Fix issue extracting install name from universal dylib
Changelog: Bugfix: Fix issue in `fix_apple_shared_install_name` handling dylib files with multiple architectures (universal).
Docs: Omit

Close: https://github.com/conan-io/conan/issues/13014

The `otool -D` command will report the install name multiple times for `dylib` files that have multiple architectures (universal binaries). In practice, the values should be the same: this PR causes it to pick the first one encountered. When subsequently calling `install_name_tool`, it will be correctly replaced in all architectures anyway.



SSE4: I don't think it's guaranteed values are always the same.
especially, for the two .dylibs built separately and then joined via `lipo`.
I suppose returning a dict `{"arch": "install_name"}` and then iterating over it is more reliable and fail proof.

Patch:
diff --git a/conan/tools/apple/apple.py b/conan/tools/apple/apple.py
index 33f9e02bb..00d82783e 100644
--- a/conan/tools/apple/apple.py
+++ b/conan/tools/apple/apple.py
@@ -159,14 +159,18 @@ class XCRun(object):
         """path to libtool"""
         return self.find('libtool')
 
+def _get_dylib_install_name(path_to_dylib):
+    command = "otool -D {}".format(path_to_dylib)
+    output =  iter(check_output_runner(command).splitlines())
+    # Note: if otool return multiple entries for different architectures
+    # assume they are the same and pick the first one.
+    for line in output:
+        if ":" in line:
+            return next(output)
+    raise ConanException(f"Unable to extract install_name for {path_to_dylib}")
 
 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 _darwin_is_binary(file, binary_type):
         if binary_type not in ("DYLIB", "EXECUTE") or os.path.islink(file) or os.path.isdir(file):
             return False
@@ -212,7 +216,7 @@ def fix_apple_shared_install_name(conanfile):
             shared_libs = _darwin_collect_binaries(full_folder, "DYLIB")
             # fix LC_ID_DYLIB in first pass
             for shared_lib in shared_libs:
-                install_name = _get_install_name(shared_lib)
+                install_name = _get_dylib_install_name(shared_lib)
                 #TODO: we probably only want to fix the install the name if
                 # it starts with `/`.
                 rpath_name = f"@rpath/{os.path.basename(install_name)}"
diff --git a/conan/tools/microsoft/__init__.py b/conan/tools/microsoft/__init__.py
index 9c0bb920d..c060c64b8 100644
--- a/conan/tools/microsoft/__init__.py
+++ b/conan/tools/microsoft/__init__.py
@@ -6,4 +6,4 @@ from conan.tools.microsoft.toolchain import MSBuildToolchain
 from conan.tools.microsoft.nmaketoolchain import NMakeToolchain
 from conan.tools.microsoft.nmakedeps import NMakeDeps
 from conan.tools.microsoft.visual import msvc_runtime_flag, VCVars, is_msvc, \
-    is_msvc_static_runtime, check_min_vs
+    is_msvc_static_runtime, check_min_vs, msvs_toolset
diff --git a/conan/tools/microsoft/toolchain.py b/conan/tools/microsoft/toolchain.py
index b98f6be45..2be8a67da 100644
--- a/conan/tools/microsoft/toolchain.py
+++ b/conan/tools/microsoft/toolchain.py
@@ -7,7 +7,7 @@ from jinja2 import Template
 from conan.tools._check_build_profile import check_using_build_profile
 from conan.tools.build import build_jobs
 from conan.tools.intel.intel_cc import IntelCC
-from conan.tools.microsoft.visual import VCVars, msvc_version_to_toolset_version
+from conan.tools.microsoft.visual import VCVars, msvs_toolset
 from conans.errors import ConanException
 from conans.util.files import save, load
 
@@ -54,7 +54,7 @@ class MSBuildToolchain(object):
         self.configuration = conanfile.settings.build_type
         self.runtime_library = self._runtime_library(conanfile.settings)
         self.cppstd = conanfile.settings.get_safe("compiler.cppstd")
-        self.toolset = self._msvs_toolset(conanfile)
+        self.toolset = msvs_toolset(conanfile)
         self.properties = {}
         check_using_build_profile(self._conanfile)
 
@@ -82,37 +82,6 @@ class MSBuildToolchain(object):
         else:
             VCVars(self._conanfile).generate()
 
-    @staticmethod
-    def _msvs_toolset(conanfile):
-        settings = conanfile.settings
-        compiler = settings.get_safe("compiler")
-        compiler_version = settings.get_safe("compiler.version")
-        if compiler == "msvc":
-            subs_toolset = settings.get_safe("compiler.toolset")
-            if subs_toolset:
-                return subs_toolset
-            return msvc_version_to_toolset_version(compiler_version)
-        if compiler == "intel":
-            compiler_version = compiler_version if "." in compiler_version else \
-                "%s.0" % compiler_version
-            return "Intel C++ Compiler " + compiler_version
-        if compiler == "intel-cc":
-            return IntelCC(conanfile).ms_toolset
-        if compiler == "Visual Studio":
-            toolset = settings.get_safe("compiler.toolset")
-            if not toolset:
-                toolsets = {"17": "v143",
-                            "16": "v142",
-                            "15": "v141",
-                            "14": "v140",
-                            "12": "v120",
-                            "11": "v110",
-                            "10": "v100",
-                            "9": "v90",
-                            "8": "v80"}
-                toolset = toolsets.get(compiler_version)
-            return toolset or ""
-
     @staticmethod
     def _runtime_library(settings):
         compiler = settings.compiler
@@ -148,7 +117,7 @@ class MSBuildToolchain(object):
 
         cppstd = "stdcpp%s" % self.cppstd if self.cppstd else ""
         runtime_library = self.runtime_library
-        toolset = self.toolset
+        toolset = self.toolset or ""
         compile_options = self._conanfile.conf.get("tools.microsoft.msbuildtoolchain:compile_options",
                                                    default={}, check_type=dict)
         self.compile_options.update(compile_options)
diff --git a/conan/tools/microsoft/visual.py b/conan/tools/microsoft/visual.py
index 5050ee8b8..1c6925949 100644
--- a/conan/tools/microsoft/visual.py
+++ b/conan/tools/microsoft/visual.py
@@ -4,6 +4,7 @@ import textwrap
 from conans.client.tools import vs_installation_path
 from conans.client.tools.version import Version
 from conans.errors import ConanException, ConanInvalidConfiguration
+from conan.tools.intel.intel_cc import IntelCC
 
 CONAN_VCVARS_FILE = "conanvcvars.bat"
 
@@ -56,7 +57,7 @@ def msvc_version_to_toolset_version(version):
                 '191': 'v141',
                 '192': 'v142',
                 "193": 'v143'}
-    return toolsets[str(version)]
+    return toolsets.get(str(version))
 
 
 class VCVars:
@@ -275,3 +276,42 @@ def is_msvc_static_runtime(conanfile):
     :return: True, if msvc + runtime MT. Otherwise, False
     """
     return is_msvc(conanfile) and "MT" in msvc_runtime_flag(conanfile)
+
+
+def msvs_toolset(conanfile):
+    """ Returns the corresponding platform toolset based on the compiler of the given conanfile.
+        In case no toolset is configured in the profile, it will return a toolset based on the
+        compiler version, otherwise, it will return the toolset from the profile.
+        When there is no compiler version neither toolset configured, it will return None
+        It supports Visual Studio, msvc and Intel.
+    :param conanfile: Conanfile instance to access settings.compiler
+    :return: A toolset when compiler.version is valid or compiler.toolset is configured. Otherwise, None.
+    """
+    settings = conanfile.settings
+    compiler = settings.get_safe("compiler")
+    compiler_version = settings.get_safe("compiler.version")
+    if compiler == "msvc":
+        subs_toolset = settings.get_safe("compiler.toolset")
+        if subs_toolset:
+            return subs_toolset
+        return msvc_version_to_toolset_version(compiler_version)
+    if compiler == "intel" and compiler_version:
+        compiler_version = compiler_version if "." in compiler_version else \
+            f"{compiler_version}.0"
+        return "Intel C++ Compiler " + compiler_version
+    if compiler == "intel-cc":
+        return IntelCC(conanfile).ms_toolset
+    if compiler == "Visual Studio":
+        toolset = settings.get_safe("compiler.toolset")
+        if not toolset:
+            toolsets = {"17": "v143",
+                        "16": "v142",
+                        "15": "v141",
+                        "14": "v140",
+                        "12": "v120",
+                        "11": "v110",
+                        "10": "v100",
+                        "9": "v90",
+                        "8": "v80"}
+            toolset = toolsets.get(str(compiler_version))
+        return toolset
diff --git a/conans/client/build/cmake.py b/conans/client/build/cmake.py
index 0006d8622..d9a668b37 100644
--- a/conans/client/build/cmake.py
+++ b/conans/client/build/cmake.py
@@ -47,8 +47,14 @@ class CMake(object):
         :param generator_platform: Generator platform name or none to autodetect (-A cmake option)
         """
         if getattr(conanfile, "must_use_new_helpers", None):
-            raise ConanException("Using the wrong 'CMake' helper. To use CMakeDeps, CMakeToolchain "
-                                 "you should use 'from conan.tools.cmake import CMake'")
+            wrong_helper_msg = "Using the wrong 'CMake' helper. To use CMakeDeps, CMakeToolchain "\
+                               "you should use 'from conan.tools.cmake import CMake'. "\
+                               "Set environment variable CONAN_DISABLE_STRICT_MODE=1 to override "\
+                               "this check (should only be used to build old packages)."
+            if get_env("CONAN_DISABLE_STRICT_MODE", False):
+                conanfile.output.warning(wrong_helper_msg)
+            else:
+                raise ConanException(wrong_helper_msg)
         self._append_vcvars = append_vcvars
         self._conanfile = conanfile
         self._settings = conanfile.settings
diff --git a/conans/client/build/msbuild.py b/conans/client/build/msbuild.py
index 778b81a76..df8b7bee4 100644
--- a/conans/client/build/msbuild.py
+++ b/conans/client/build/msbuild.py
@@ -23,9 +23,14 @@ class MSBuild(object):
     def __init__(self, conanfile):
         if isinstance(conanfile, ConanFile):
             if getattr(conanfile, "must_use_new_helpers", None):
-                raise ConanException(
-                    "Using the wrong 'MSBuild' helper. To use MSBuildDeps, MSBuildToolchain "
-                    "you should use 'from conan.tools.microsoft import MSBuild'")
+                wrong_helper_msg = "Using the wrong 'MSBuild' helper. To use MSBuildDeps, MSBuildToolchain "\
+                                   "you should use 'from conan.tools.microsoft import MSBuild'. "\
+                                   "Set environment variable CONAN_DISABLE_STRICT_MODE=1 to override "\
+                                   "this check (should only be used to build old packages)."
+                if get_env("CONAN_DISABLE_STRICT_MODE", False):
+                    conanfile.output.warning(wrong_helper_msg)
+                else:
+                    raise ConanException(wrong_helper_msg)
             self._conanfile = conanfile
             self._settings = self._conanfile.settings
             self._output = self._conanfile.output


Test patch:
diff --git a/conans/test/unittests/tools/apple/test_apple_tools.py b/conans/test/unittests/tools/apple/test_apple_tools.py
index 78dd0e93d..18622458a 100644
--- a/conans/test/unittests/tools/apple/test_apple_tools.py
+++ b/conans/test/unittests/tools/apple/test_apple_tools.py
@@ -1,8 +1,11 @@
+import mock
 import pytest
+import textwrap
 
 from conans.test.utils.mocks import ConanFileMock, MockSettings
 from conans.test.utils.test_files import temp_folder
 from conan.tools.apple import is_apple_os, to_apple_arch, fix_apple_shared_install_name, XCRun
+from conan.tools.apple.apple import _get_dylib_install_name # testing private function
 
 def test_tools_apple_is_apple_os():
     conanfile = ConanFileMock()
@@ -48,3 +51,23 @@ def test_xcrun_public_settings():
     settings = xcrun.settings
 
     assert settings.os == "watchOS"
+
+def test_get_dylib_install_name():
+    # https://github.com/conan-io/conan/issues/13014
+    single_arch = textwrap.dedent("""
+    /path/to/libwebp.7.dylib:
+    /absolute/path/lib/libwebp.7.dylib
+    """)
+
+    universal_binary = textwrap.dedent("""
+    /.conan/data/package/lib/libwebp.7.dylib (architecture x86_64):
+    /absolute/path/lib/libwebp.7.dylib
+    /.conan/data/package/lib/libwebp.7.dylib (architecture arm64):
+    /absolute/path/lib/libwebp.7.dylib
+    """)
+
+    for mock_output in (single_arch, universal_binary):
+        with mock.patch("conan.tools.apple.apple.check_output_runner") as mock_output_runner:
+            mock_output_runner.return_value = mock_output
+            install_name = _get_dylib_install_name("/path/to/libwebp.7.dylib")
+            assert "/absolute/path/lib/libwebp.7.dylib" == install_name
diff --git a/conans/test/unittests/tools/microsoft/test_msvs_toolset.py b/conans/test/unittests/tools/microsoft/test_msvs_toolset.py
new file mode 100644
index 000000000..de1ec93b0
--- /dev/null
+++ b/conans/test/unittests/tools/microsoft/test_msvs_toolset.py
@@ -0,0 +1,141 @@
+import pytest
+from conan.tools.microsoft import msvs_toolset
+from conan.errors import ConanException
+from conans.test.utils.mocks import ConanFileMock, MockSettings
+
+
+@pytest.mark.parametrize("compiler_version,expected_toolset", [
+    ("17", "v143"),
+    ("16", "v142"),
+    ("15", "v141"),
+    ("14", "v140"),
+    ("12", "v120"),
+    ("11", "v110"),
+    ("10", "v100"),
+    ("9", "v90"),
+    ("8", "v80")])
+def test_visual_studio_default(compiler_version, expected_toolset):
+    """ When running Visual Studio as compiler, and there is no toolset configured,
+        msvs_toolset must return a specific version based on the compiler version
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "Visual Studio", "compiler.version": compiler_version})
+    assert msvs_toolset(conanfile) == expected_toolset
+
+
+@pytest.mark.parametrize("compiler_version,toolset,expected_toolset", [
+    ("16", "v142_xp", "v142_xp"),
+    ("15", "v141_xp", "v141_xp"),
+    ("14", "v140_xp", "v140_xp"),
+    ("12", "v120_xp", "v120_xp"),
+    ("11", "v110_xp", "v110_xp")])
+def test_visual_studio_custom(compiler_version, toolset, expected_toolset):
+    """ When running Visual Studio as compiler, and there is a toolset configured,
+        msvs_toolset must return the specified toolset from profile
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "Visual Studio",
+                             "compiler.version": compiler_version,
+                             "compiler.toolset": toolset})
+    assert msvs_toolset(conanfile) == expected_toolset
+
+
+def test_invalid_compiler():
+    """ When the compiler version is unknown and there is no toolset configured,
+        msvs_toolset must return None
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "Visual Studio",
+                                       "compiler.version": "20220203"})
+    assert msvs_toolset(conanfile) is None
+
+
+@pytest.mark.parametrize("compiler_version,expected_toolset", [
+    ("193", "v143"),
+    ("192", "v142"),
+    ("191", "v141"),
+    ("190", "v140"),
+    ("180", "v120"),
+    ("170", "v110")])
+def test_msvc_default(compiler_version, expected_toolset):
+    """ When running msvc as compiler, and there is no toolset configured,
+        msvs_toolset must return a specific version based on the compiler version
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "msvc", "compiler.version": compiler_version})
+    assert msvs_toolset(conanfile) == expected_toolset
+
+
+@pytest.mark.parametrize("compiler_version,toolset,expected_toolset", [
+    ("193", "v143_xp", "v143_xp"),
+    ("192", "v142_xp", "v142_xp"),
+    ("191", "v141_xp", "v141_xp"),
+    ("190", "v140_xp", "v140_xp"),
+    ("180", "v120_xp", "v120_xp"),
+    ("170", "v110_xp", "v110_xp")])
+def test_msvc_custom(compiler_version, toolset, expected_toolset):
+    """ When running msvc as compiler, and there is a toolset configured,
+        msvs_toolset must return the specified toolset from profile
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "msvc",
+                             "compiler.version": compiler_version,
+                             "compiler.toolset": toolset})
+    assert msvs_toolset(conanfile) == expected_toolset
+
+
+@pytest.mark.parametrize("compiler_version,expected_toolset", [
+    ("19.1", "Intel C++ Compiler 19.1"),
+    ("19", "Intel C++ Compiler 19.0"),
+    ("18", "Intel C++ Compiler 18.0"),
+    ("17", "Intel C++ Compiler 17.0")])
+def test_intel_default(compiler_version, expected_toolset):
+    """ When running intel as compiler, and there is a compiler version configured,
+        msvs_toolset must return the compiler version as its toolset
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "intel", "compiler.version": compiler_version})
+    assert msvs_toolset(conanfile) == expected_toolset
+
+
+def test_intel_invalid():
+    """ When running intel as compiler, and there is no compiler version configured,
+        msvs_toolset must return None
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "intel"})
+    assert msvs_toolset(conanfile) is None
+
+
+def test_intel_cc_old_compiler():
+    """ When running intel-cc as compiler, and the compiler version configured is older than 2021
+        msvs_toolset must raise an ConanExpection
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "intel-cc", "compiler.version": "19.1"})
+    with pytest.raises(ConanException) as error:
+        msvs_toolset(conanfile)
+    assert "You have to use 'intel' compiler" in str(error.value)
+
+
+@pytest.mark.parametrize("compiler_version,compiler_mode,expected_toolset", [
+    ("2021.3", "classic", "Intel C++ Compiler 19.2"),
+    ("2021.2", "classic", "Intel C++ Compiler 19.2"),
+    ("2021.1", "classic", "Intel C++ Compiler 19.2"),
+    ("2021.3", "icx", "Intel C++ Compiler 2021"),
+    ("2021.2", "icx", "Intel C++ Compiler 2021"),
+    ("2021.1", "icx", "Intel C++ Compiler 2021"),
+    ("2021.3", "dpcpp", "Intel(R) oneAPI DPC++ Compiler"),
+    ("2021.2", "dpcpp", "Intel(R) oneAPI DPC++ Compiler"),
+    ("2021.1", "dpcpp", "Intel(R) oneAPI DPC++ Compiler"),
+    ("2021.3", None, "Intel(R) oneAPI DPC++ Compiler")])
+def test_intel_cc_default(compiler_version, compiler_mode, expected_toolset):
+    """ When running intel-cc as compiler, and there is a proper compiler version configured,
+        and the compiler.mode is configured,
+        msvs_toolset must return a compiler toolset based on compiler.mode
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "intel-cc",
+                                       "compiler.version": compiler_version,
+                                       "compiler.mode": compiler_mode})
+    assert msvs_toolset(conanfile) == expected_toolset