From 8f12d5cccbfb7be930a7749ab1b0a3b4fbcbc754 Mon Sep 17 00:00:00 2001 From: Fletcher D Date: Fri, 12 Jul 2024 09:52:17 -0700 Subject: [PATCH] [PR-182][Bug] Loading library on Aarch64 fails because pylink attempts to load 32-bit library (#182) The ARM64 version of the JLink software contains two library files, `libjlinkarm.so` (64 bit) and `libjlinkarm_arm.so` (32 bit). Pylink attempts to load `libjlinkarm_arm.so` first, which fails on ARM64 due to the architecture mismatch and causes an error. This change simply performs a 'test load' of each library file found, skipping if it fails, fixing this issue on ARM64. --- pylink/library.py | 21 +++++++++++++- tests/unit/test_library.py | 57 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/pylink/library.py b/pylink/library.py index df50a41..ca4d33f 100644 --- a/pylink/library.py +++ b/pylink/library.py @@ -138,6 +138,23 @@ def get_appropriate_windows_sdk_name(cls): else: return Library.WINDOWS_32_JLINK_SDK_NAME + @classmethod + def can_load_library(cls, dllpath): + """Test whether a library is the correct architecture to load. + + Args: + cls (Library): the ``Library`` class + dllpath (str): A path to a library. + + Returns: + ``True`` if the library could be successfully loaded, ``False`` if not. + """ + try: + ctypes.CDLL(dllpath) + return True + except OSError: + return False + @classmethod def find_library_windows(cls): """Loads the SEGGER DLL from the windows installation directory. @@ -203,7 +220,9 @@ def find_library_linux(cls): for fname in fnames: fpath = os.path.join(directory_name, fname) if util.is_os_64bit(): - if '_x86' not in fname: + if not cls.can_load_library(fpath): + continue + elif '_x86' not in fname: yield fpath elif x86_found: if '_x86' in fname: diff --git a/tests/unit/test_library.py b/tests/unit/test_library.py index fb92f92..e12f967 100644 --- a/tests/unit/test_library.py +++ b/tests/unit/test_library.py @@ -880,13 +880,16 @@ def test_linux_6_10_0_32bit(self, mock_os, mock_load_library, mock_find_library, @mock.patch('tempfile.NamedTemporaryFile', new=mock.Mock()) @mock.patch('ctypes.util.find_library') @mock.patch('ctypes.cdll.LoadLibrary') + @mock.patch('ctypes.CDLL') @mock.patch('pylink.library.os') - def test_linux_6_10_0_64bit(self, mock_os, mock_load_library, mock_find_library, mock_open, mock_is_os_64bit): + def test_linux_6_10_0_64bit(self, mock_os, mock_cdll, mock_load_library, + mock_find_library, mock_open, mock_is_os_64bit): """Tests finding the DLL on Linux through the SEGGER application for V6.0.0+ on 64 bit linux. Args: self (TestLibrary): the ``TestLibrary`` instance mock_os (Mock): a mocked version of the ``os`` module + mock_cdll (Mock): a mocked version of the `cdll.CDLL` class constructor mock_load_library (Mock): a mocked version of the library loader mock_find_library (Mock): a mocked call to ``ctypes`` find library mock_open (Mock): mock for mocking the call to ``open()`` @@ -896,6 +899,7 @@ def test_linux_6_10_0_64bit(self, mock_os, mock_load_library, mock_find_library, ``None`` """ mock_find_library.return_value = None + mock_cdll.return_value = None directories = [ '/opt/SEGGER/JLink_Linux_V610d_x86_64/libjlinkarm_x86.so.6.10', '/opt/SEGGER/JLink_Linux_V610d_x86_64/libjlinkarm.so.6.10', @@ -918,6 +922,49 @@ def test_linux_6_10_0_64bit(self, mock_os, mock_load_library, mock_find_library, lib.unload = mock.Mock() self.assertEqual(None, lib._path) + @mock.patch('sys.platform', new='linux2') + @mock.patch('pylink.util.is_os_64bit', return_value=True) + @mock.patch('pylink.library.open') + @mock.patch('os.remove', new=mock.Mock()) + @mock.patch('tempfile.NamedTemporaryFile', new=mock.Mock()) + @mock.patch('ctypes.util.find_library') + @mock.patch('ctypes.cdll.LoadLibrary') + @mock.patch('ctypes.CDLL') + @mock.patch('pylink.library.os') + def test_linux_64bit_no_x86(self, mock_os, mock_cdll, mock_load_library, + mock_find_library, mock_open, mock_is_os_64bit): + """Tests finding the DLL on Linux when no library name contains 'x86'. + + Args: + self (TestLibrary): the ``TestLibrary`` instance + mock_os (Mock): a mocked version of the ``os`` module + mock_cdll (Mock): a mocked version of the `cdll.CDLL` class constructor + mock_load_library (Mock): a mocked version of the library loader + mock_find_library (Mock): a mocked call to ``ctypes`` find library + mock_open (Mock): mock for mocking the call to ``open()`` + mock_is_os_64bit (Mock): mock for mocking the call to ``is_os_64bit``, returns True + + Returns: + ``None`` + """ + def on_cdll(name): + if '_arm' in name: + raise OSError + + mock_find_library.return_value = None + mock_cdll.side_effect = on_cdll + directories = [ + '/opt/SEGGER/JLink_Linux_V610d_x86_64/libjlinkarm_arm.so.6.10', + '/opt/SEGGER/JLink_Linux_V610d_x86_64/libjlinkarm.so.6.10', + ] + + self.mock_directories(mock_os, directories, '/') + + lib = library.Library() + lib.unload = mock.Mock() + load_library_args, load_libary_kwargs = mock_load_library.call_args + self.assertEqual(directories[1], lib._path) + @mock.patch('sys.platform', new='linux') @mock.patch('pylink.library.open') @mock.patch('os.remove', new=mock.Mock()) @@ -960,8 +1007,9 @@ def test_linux_empty(self, mock_os, mock_load_library, mock_find_library, mock_o @mock.patch('pylink.platform.libc_ver', return_value=('libc', '1.0')) @mock.patch('ctypes.util.find_library', return_value='libjlinkarm.so.7') @mock.patch('pylink.library.JLinkarmDlInfo.__init__') + @mock.patch('ctypes.CDLL') @mock.patch('ctypes.cdll.LoadLibrary') - def test_linux_glibc_unavailable(self, mock_load_library, mock_dlinfo_ctr, mock_find_library, + def test_linux_glibc_unavailable(self, mock_load_library, mock_cdll, mock_dlinfo_ctr, mock_find_library, mock_libc_ver, mock_is_os_64bit, mock_os, mock_open): """Confirms the whole JLinkarmDlInfo code path is not involved when GNU libc extensions are unavailable on a Linux system, and that we'll successfully fallback @@ -974,6 +1022,7 @@ def test_linux_glibc_unavailable(self, mock_load_library, mock_dlinfo_ctr, mock_ to the "search by file name" code path, aka find_library_linux() - and "successfully load" a mock library file from /opt/SEGGER/JLink """ + mock_cdll.side_effect = None directories = [ # Library.find_library_linux() should find this. '/opt/SEGGER/JLink/libjlinkarm.so.6' @@ -999,8 +1048,9 @@ def test_linux_glibc_unavailable(self, mock_load_library, mock_dlinfo_ctr, mock_ @mock.patch('pylink.util.is_os_64bit', return_value=True) @mock.patch('pylink.platform.libc_ver', return_value=('glibc', '2.34')) @mock.patch('ctypes.util.find_library') + @mock.patch('ctypes.CDLL') @mock.patch('ctypes.cdll.LoadLibrary') - def test_linux_dl_unavailable(self, mock_load_library, mock_find_library, mock_libc_ver, + def test_linux_dl_unavailable(self, mock_load_library, mock_cdll, mock_find_library, mock_libc_ver, mock_is_os_64bit, mock_os, mock_open): """Confirms we successfully fallback to the "search by file name" code path when libdl is unavailable despite the host system presenting itself as POSIX (GNU/Linux). @@ -1012,6 +1062,7 @@ def test_linux_dl_unavailable(self, mock_load_library, mock_find_library, mock_l to the "search by file name" code path, aka find_library_linux() - and "successfully load" a mock library file from /opt/SEGGER/JLink """ + mock_cdll.side_effect = None mock_find_library.side_effect = [ # find_library('jlinkarm') 'libjlinkarm.so.6',