Repository: pyvisa/pyvisa

Issue:
Macbook , They are all 64bits, but tips 'no matching architecture in universal wrapper'
Machine Details:
   Platform ID:    macOS-11.2.3-arm64-arm-64bit
   Processor:      arm

Python:
   Implementation: CPython
   Executable:     /opt/homebrew/opt/python@3.9/bin/python3.9
   Version:        3.9.9
   Compiler:       Clang 13.0.0 (clang-1300.0.29.3)
   Bits:           64bit
   Build:          Nov 21 2021 03:16:13 (#main)
   Unicode:        UCS4

PyVISA Version: 1.11.3

Backends:
   ivi:
      Version: 1.11.3 (bundled with PyVISA)
      #1: /Library/Frameworks/visa.framework/visa:
         found by: auto
         bitness: 64
         Could not get more info: Interpreter and library have different bitness.


import pyvisa
rm = pyvisa.ResourceManager('/Library/Frameworks/VISA.framework/VISA')
rm.list_resources()

OSError: dlopen(/Library/Frameworks/VISA.framework/VISA, 6): no suitable image found.  Did find:
	/Library/Frameworks/VISA.framework/VISA: no matching architecture in universal wrapper
	/Library/Frameworks/VISA.framework/VISA: no matching architecture in universal wrapper
	/Library/Frameworks/VISA.framework/Versions/A/VISA: no matching architecture in universal wrapper

Discussion:
arr-ee: I'm not sure why mypy suddenly decided to act on an issue that appears to have been there for some 14 months or so. Am I missing something, or shall I open an issue to address it?
MatthieuDartiailh: Mypy has become stricter recently. Feel free to address the issue in this PR.
arr-ee: > explain to me why you switched from tmpdir to tmp_path (for my own curiosity)

Apologies for that change, it's more of a bugbear of mine. `Path` provides a surprisingly ergonomic (not to mention type-checked and reasonably cross-platform) API for working with file system, and I've been habitually updating the codebase as I was going with no regard to introducing inconsistencies. I'm OK with either reverting that part of the changeset or filing a follow-up to update the rest of the lib — up to you.

While we're talking about builds suddenly failing due to stealth mypy upgrade: any opinions on `pipenv`/another way to have lock-files for dependencies?

Thanks!
MatthieuDartiailh: I am fine with the change. I must have missed the introduction of that fixture (I remember being bitten often enough by tmpdir that I do not mind the change).

Honestly, I prefer mypy flagging incorrect/dubious things. I can take care of it if you want, but having to update pipenv myself would likely result in quite a lag and I prefer to avoid that.

Patch:
diff --git a/CHANGES b/CHANGES
index 90d6ded..8de9ba9 100644
--- a/CHANGES
+++ b/CHANGES
@@ -5,6 +5,15 @@ PyVISA Changelog
 -------------------
 - fix ctypes truncated pointers on 64-bit for ViBusAddress, ViBusSize, ViAttrState PR # 725
 - fix read_termination_context does not allow None PR # 727
+- replace `bitness` for library and python runtime information with instruction set
+  and bitness # 733
+  Resulting API changes:
+  - `pyvisa.util.LibraryPath.arch` now returns a list of `pyvisa.util.ArchitectureType`
+    enum values representing architectures the binary was compiled for
+  - `p.u.LibraryPath.is_32bit`, `p.u.LibraryPath.is_64bit`, and `p.u.LibraryPath.bitness`
+    have been removed
+  - `p.u.get_shared_library_arch` now returns `p.u.PEMachineType` instead of a string
+  - `p.u.get_arch` now returns a list of `p.u.ArchitectureType`
 
 1.13.0 (22-12-2022)
 -------------------
diff --git a/pyvisa/ctwrapper/highlevel.py b/pyvisa/ctwrapper/highlevel.py
index 6f8418d..38c8f65 100644
--- a/pyvisa/ctwrapper/highlevel.py
+++ b/pyvisa/ctwrapper/highlevel.py
@@ -121,7 +121,7 @@ class IVIVisaLibrary(highlevel.VisaLibraryBase):
         for ndx, visalib in enumerate(paths, 1):
             nfo: Dict[str, Union[str, List[str]]] = OrderedDict()
             nfo["found by"] = visalib.found_by
-            nfo["bitness"] = visalib.bitness
+            nfo["architecture"] = [str(a.value) for a in visalib.arch]
             try:
                 lib = cls(visalib)
                 sess, _ = lib.open_default_resource_manager()
diff --git a/pyvisa/errors.py b/pyvisa/errors.py
index d271a4d..fe1e64d 100644
--- a/pyvisa/errors.py
+++ b/pyvisa/errors.py
@@ -730,9 +730,12 @@ class LibraryError(OSError, Error):
             filename, "user" if filename == util.read_user_library_path() else "auto"
         )
         s += "No matching architecture.\n"
-        s += "    Current Python interpreter is %s bits\n" % details["bits"]
+        s += (
+            "    Current Python interpreter architecture: %s\n"
+            % details["architecture"]
+        )
         s += "    The library in: %s\n" % visalib.path
         s += "      found by: %s\n" % visalib.found_by
-        s += "      bitness: %s\n" % visalib.bitness
+        s += "      architecture: %s\n" % [str(a.value) for a in visalib.arch]
 
         return cls("Error while accessing %s: %s" % (filename, s))
diff --git a/pyvisa/util.py b/pyvisa/util.py
index f40cc0b..6b630ca 100644
--- a/pyvisa/util.py
+++ b/pyvisa/util.py
@@ -18,6 +18,8 @@ import subprocess
 import sys
 import warnings
 from collections import OrderedDict
+from enum import Enum
+from pathlib import Path
 from types import ModuleType
 from typing import (
     Any,
@@ -161,6 +163,56 @@ def add_user_dll_extra_paths() -> Optional[List[str]]:
         return None
 
 
+class PEMachineType(Enum):
+    UNKNOWN = 0
+    I386 = 0x014C
+    R3000 = 0x0162
+    R4000 = 0x0166
+    R10000 = 0x0168
+    WCEMIPSV2 = 0x0169
+    ALPHA = 0x0184
+    SH3 = 0x01A2
+    SH3DSP = 0x01A3
+    SH3E = 0x01A4
+    SH4 = 0x01A6
+    SH5 = 0x01A8
+    ARM = 0x01C0
+    AARCH64 = 0xAA64
+    THUMB = 0x01C2
+    ARMNT = 0x01C4
+    AM33 = 0x01D3
+    POWERPC = 0x01F0
+    POWERPCFP = 0x01F1
+    IA64 = 0x0200
+    MIPS16 = 0x0266
+    ALPHA64 = 0x0284
+    MIPSFPU = 0x0366
+    MIPSFPU16 = 0x0466
+    TRICORE = 0x0520
+    CEF = 0x0CEF
+    EBC = 0x0EBC
+    AMD64 = 0x8664
+    M32R = 0x9041
+    CEE = 0xC0EE
+
+
+class ArchitectureType(Enum):
+    I386 = ("x86", 32)
+    X86_64 = ("x86", 64)
+    AARCH64 = ("arm", 64)
+
+    @classmethod
+    def from_platform_machine(cls, machine: str) -> Optional["ArchitectureType"]:
+        if machine == "i386" or machine == "i686":
+            return cls.I386
+        elif machine == "x86_64" or machine == "amd64":
+            return cls.X86_64
+        elif machine == "arm64" or machine == "aarch64":
+            return cls.AARCH64
+
+        return None
+
+
 class LibraryPath(str):
     """Object encapsulating information about a VISA dynamic library."""
 
@@ -170,8 +222,8 @@ class LibraryPath(str):
     #: Detection method employed to locate the library
     found_by: str
 
-    #: Architectural information (32, ) or (64, ) or (32, 64)
-    _arch: Optional[Tuple[int, ...]] = None
+    #: Architectural information
+    _arch: Optional[List[ArchitectureType]] = None
 
     def __new__(
         cls: Type["LibraryPath"], path: str, found_by: str = "auto"
@@ -183,37 +235,16 @@ class LibraryPath(str):
         return obj
 
     @property
-    def arch(self) -> Tuple[int, ...]:
+    def arch(self) -> List[ArchitectureType]:
         """Architecture of the library."""
         if self._arch is None:
             try:
-                self._arch = get_arch(self.path)
+                self._arch = get_arch(Path(self.path))
             except Exception:
-                self._arch = tuple()
+                self._arch = []
 
         return self._arch
 
-    @property
-    def is_32bit(self) -> Union[bool, Literal["n/a"]]:
-        """Is the library 32 bits."""
-        if not self.arch:
-            return "n/a"
-        return 32 in self.arch
-
-    @property
-    def is_64bit(self) -> Union[bool, Literal["n/a"]]:
-        """Is the library 64 bits."""
-        if not self.arch:
-            return "n/a"
-        return 64 in self.arch
-
-    @property
-    def bitness(self) -> str:
-        """Bitness of the library."""
-        if not self.arch:
-            return "n/a"
-        return ", ".join(str(a) for a in self.arch)
-
 
 def cleanup_timeout(timeout: Optional[Union[int, float]]) -> int:
     """Turn a timeout expressed as a float into in interger or the proper constant."""
@@ -808,7 +839,12 @@ def get_system_details(
     else:
         # UCS4 build (most recent Linux distros)
         unitype = "UCS4"
-    bits, linkage = platform.architecture()
+    machine_info = platform.machine()
+    maybe_architecture = ArchitectureType.from_platform_machine(machine_info)
+    if maybe_architecture:
+        architecture_str = str(maybe_architecture.value)
+    else:
+        architecture_str = machine_info
 
     from . import __version__
 
@@ -823,7 +859,7 @@ def get_system_details(
         "buildno": buildno,
         "builddate": builddate,
         "unicode": unitype,
-        "bits": bits,
+        "architecture": architecture_str,
         "pyvisa": __version__,
         "backends": backend_details,
     }
@@ -855,7 +891,9 @@ def get_system_details(
     return d
 
 
-def system_details_to_str(d: Dict[str, str], indent: str = "") -> str:
+def system_details_to_str(
+    d: Dict[str, Union[str, Dict[str, DebugInfo]]], indent: str = ""
+) -> str:
     """Convert the system details to a str.
 
     System details can be obtained by `get_system_details`.
@@ -872,7 +910,7 @@ def system_details_to_str(d: Dict[str, str], indent: str = "") -> str:
         "   Executable:     %s" % d.get("executable", "n/a"),
         "   Version:        %s" % d.get("python", "n/a"),
         "   Compiler:       %s" % d.get("compiler", "n/a"),
-        "   Bits:           %s" % d.get("bits", "n/a"),
+        "   Architecture:   %s" % d.get("architecture", "n/a"),
         "   Build:          %s (#%s)"
         % (d.get("builddate", "n/a"), d.get("buildno", "n/a")),
         "   Unicode:        %s" % d.get("unicode", "n/a"),
@@ -930,7 +968,7 @@ def get_debug_info(to_screen: Literal[False]) -> str:
     pass
 
 
-def get_debug_info(to_screen=True):
+def get_debug_info(to_screen: bool = True):
     """Get the PyVISA debug information."""
     out = system_details_to_str(get_system_details())
     if not to_screen:
@@ -938,41 +976,7 @@ def get_debug_info(to_screen=True):
     print(out)
 
 
-machine_types = {
-    0: "UNKNOWN",
-    0x014C: "I386",
-    0x0162: "R3000",
-    0x0166: "R4000",
-    0x0168: "R10000",
-    0x0169: "WCEMIPSV2",
-    0x0184: "ALPHA",
-    0x01A2: "SH3",
-    0x01A3: "SH3DSP",
-    0x01A4: "SH3E",
-    0x01A6: "SH4",
-    0x01A8: "SH5",
-    0x01C0: "ARM",
-    0x01C2: "THUMB",
-    0x01C4: "ARMNT",
-    0x01D3: "AM33",
-    0x01F0: "POWERPC",
-    0x01F1: "POWERPCFP",
-    0x0200: "IA64",
-    0x0266: "MIPS16",
-    0x0284: "ALPHA64",
-    # 0x0284: 'AXP64', # same
-    0x0366: "MIPSFPU",
-    0x0466: "MIPSFPU16",
-    0x0520: "TRICORE",
-    0x0CEF: "CEF",
-    0x0EBC: "EBC",
-    0x8664: "AMD64",
-    0x9041: "M32R",
-    0xC0EE: "CEE",
-}
-
-
-def get_shared_library_arch(filename: str) -> str:
+def get_shared_library_arch(filename: Union[str, Path]) -> PEMachineType:
     """Get the architecture of shared library."""
     with io.open(filename, "rb") as fp:
         dos_headers = fp.read(64)
@@ -991,35 +995,64 @@ def get_shared_library_arch(filename: str) -> str:
         if sig != b"PE":
             raise Exception("Not a PE executable")
 
-        return machine_types.get(machine, "UNKNOWN")
+        try:
+            return PEMachineType(machine)
+        except ValueError:
+            return PEMachineType.UNKNOWN
 
 
-def get_arch(filename: str) -> Tuple[int, ...]:
+def get_arch(filename: Union[str, Path]) -> List[ArchitectureType]:
     """Get the architecture of the platform."""
     this_platform = sys.platform
     if this_platform.startswith("win"):
         machine_type = get_shared_library_arch(filename)
-        if machine_type == "I386":
-            return (32,)
-        elif machine_type in ("IA64", "AMD64"):
-            return (64,)
+        if machine_type == PEMachineType.I386:
+            return [ArchitectureType.I386]
+        elif machine_type == PEMachineType.AMD64:
+            return [ArchitectureType.X86_64]
+        elif machine_type == PEMachineType.AARCH64:
+            return [ArchitectureType.AARCH64]
         else:
-            return ()
-    elif this_platform not in ("linux2", "linux3", "linux", "darwin"):
+            return []
+    elif this_platform not in ("linux", "darwin"):
         raise OSError("Unsupported platform: %s" % this_platform)
-
     res = subprocess.run(["file", filename], capture_output=True)
     out = res.stdout.decode("ascii")
-    ret = []
+
     if this_platform.startswith("linux"):
-        if "32-bit" in out:
-            ret.append(32)
-        if "64-bit" in out:
-            ret.append(64)
+        # Example outputs:
+        #   i386:
+        #       /usr/bin/python: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV)
+        #   x86_64:
+        #       /usr/bin/python3.10: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked
+        #   aarch64:
+        #       /usr/bin/python3.9: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked
+        if "80386" in out:
+            return [ArchitectureType.I386]
+        if "x86-64" in out:
+            return [ArchitectureType.X86_64]
+        if "aarch64" in out:
+            return [ArchitectureType.AARCH64]
+
+        return []
     else:  # darwin
-        if "(for architecture i386)" in out:
-            ret.append(32)
-        if "(for architecture x86_64)" in out:
-            ret.append(64)
-
-    return tuple(ret)
+        # universal binary, i386 and x86_64:
+        #   /usr/bin/grep: Mach-O universal binary with 2 architectures
+        #   /usr/bin/grep (for architecture x86_64):    Mach-O 64-bit executable x86_64
+        #   /usr/bin/grep (for architecture i386):      Mach-O executable i386
+        # universal binary, x86_64 and aarch64:
+        #   /usr/bin/grep: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64e:Mach-O 64-bit executable arm64e]
+        #   /usr/bin/grep (for architecture x86_64):	Mach-O 64-bit executable x86_64
+        #   /usr/bin/grep (for architecture arm64e):	Mach-O 64-bit executable arm64e
+        # single-arch binary, aarch64:
+        #   /opt/homebrew/bin/rg: Mach-O 64-bit executable arm64
+        archs: List[ArchitectureType] = []
+
+        if "executable i386" in out:
+            archs.append(ArchitectureType.I386)
+        if "executable x86_64" in out:
+            archs.append(ArchitectureType.X86_64)
+        if "executable arm64" in out:
+            archs.append(ArchitectureType.AARCH64)
+
+        return archs


Test patch:
diff --git a/pyvisa/testsuite/test_util.py b/pyvisa/testsuite/test_util.py
index e80577d..9ff493d 100644
--- a/pyvisa/testsuite/test_util.py
+++ b/pyvisa/testsuite/test_util.py
@@ -14,6 +14,7 @@ import unittest
 from configparser import ConfigParser
 from functools import partial
 from io import StringIO
+from pathlib import Path
 from types import ModuleType
 from typing import Optional
 
@@ -509,26 +510,29 @@ class TestSystemDetailsAnalysis(BaseTestCase):
         util.system_details_to_str(details)
 
 
-def generate_fakelibs(dirname):
-    for name, blob in zip(
-        [
-            "fakelib_bad_magic.dll",
-            "fakelib_good_32.dll",
-            "fakelib_good_64_2.dll",
-            "fakelib_good_64.dll",
-            "fakelib_good_unknown.dll",
-            "fakelib_not_pe.dll",
-        ],
-        [
-            struct.pack("=6sH52sl", b"MAPE\x00\x00", 0x014C, 52 * b"\0", 2),
-            struct.pack("=6sH52sl", b"MZPE\x00\x00", 0x014C, 52 * b"\0", 2),
-            struct.pack("=6sH52sl", b"MZPE\x00\x00", 0x8664, 52 * b"\0", 2),
-            struct.pack("=6sH52sl", b"MZPE\x00\x00", 0x0200, 52 * b"\0", 2),
-            struct.pack("=6sH52sl", b"MZPE\x00\x00", 0xFFFF, 52 * b"\0", 2),
-            struct.pack("=6sH52sl", b"MZDE\x00\x00", 0x014C, 52 * b"\0", 2),
-        ],
-    ):
-        with open(os.path.join(dirname, name), "wb") as f:
+def generate_fakelibs(dirname: Path):
+    libs = {
+        "fakelib_bad_magic.dll": struct.pack(
+            "=6sH52sl", b"MAPE\x00\x00", 0x014C, 52 * b"\0", 2
+        ),
+        "fakelib_good_x86_32.dll": struct.pack(
+            "=6sH52sl", b"MZPE\x00\x00", 0x014C, 52 * b"\0", 2
+        ),
+        "fakelib_good_x86_64.dll": struct.pack(
+            "=6sH52sl", b"MZPE\x00\x00", 0x8664, 52 * b"\0", 2
+        ),
+        "fakelib_good_arm_64.dll": struct.pack(
+            "=6sH52sl", b"MZPE\x00\x00", 0xAA64, 52 * b"\0", 2
+        ),
+        "fakelib_good_unknown.dll": struct.pack(
+            "=6sH52sl", b"MZPE\x00\x00", 0xFFFF, 52 * b"\0", 2
+        ),
+        "fakelib_not_pe.dll": struct.pack(
+            "=6sH52sl", b"MZDE\x00\x00", 0x014C, 52 * b"\0", 2
+        ),
+    }
+    for name, blob in libs.items():
+        with open(dirname / name, "wb") as f:
             f.write(blob)
             print("Written %s" % name)
 
@@ -536,53 +540,52 @@ def generate_fakelibs(dirname):
 class TestLibraryAnalysis(BaseTestCase):
     """Test (through monkey patching) the analysis of binary libraries."""
 
-    def test_get_shared_library_arch(self, tmpdir):
+    def test_get_shared_library_arch(self, tmp_path: Path):
         """Test analysing a library on Windows."""
-        dirname = str(tmpdir)
-        generate_fakelibs(dirname)
-
-        for f, a in zip(["_32", "_64", "_64_2"], ["I386", "IA64", "AMD64"]):
-            arch = util.get_shared_library_arch(
-                os.path.join(tmpdir, "fakelib_good%s.dll" % f)
-            )
+        generate_fakelibs(tmp_path)
+
+        for f, a in zip(
+            ["x86_32", "x86_64", "arm_64"],
+            [
+                util.PEMachineType.I386,
+                util.PEMachineType.AMD64,
+                util.PEMachineType.AARCH64,
+            ],
+        ):
+            arch = util.get_shared_library_arch(tmp_path / f"fakelib_good_{f}.dll")
             assert arch == a
 
-        arch = util.get_shared_library_arch(
-            os.path.join(dirname, "fakelib_good_unknown.dll")
-        )
-        assert arch == "UNKNOWN"
+        arch = util.get_shared_library_arch(tmp_path / "fakelib_good_unknown.dll")
+        assert arch == util.PEMachineType.UNKNOWN
 
         with pytest.raises(Exception) as e:
-            util.get_shared_library_arch(os.path.join(dirname, "fakelib_bad_magic.dll"))
+            util.get_shared_library_arch(tmp_path / "fakelib_bad_magic.dll")
         assert "Not an executable" in e.exconly()
 
         with pytest.raises(Exception) as e:
-            util.get_shared_library_arch(os.path.join(dirname, "fakelib_not_pe.dll"))
+            util.get_shared_library_arch(tmp_path / "fakelib_not_pe.dll")
         assert "Not a PE executable" in e.exconly()
 
-    def test_get_arch_windows(self, tmpdir):
+    def test_get_arch_windows(self, tmp_path: Path):
         """Test identifying the computer architecture on windows."""
-        dirname = str(tmpdir)
-        generate_fakelibs(dirname)
+        generate_fakelibs(tmp_path)
 
         platform = sys.platform
         sys.platform = "win32"
         try:
             for f, a in zip(
-                ["_32", "_64", "_64_2", "_unknown"], [(32,), (64,), (64,), ()]
+                ["x86_32", "x86_64", "arm_64", "_unknown"],
+                [
+                    [util.ArchitectureType.I386],
+                    [util.ArchitectureType.X86_64],
+                    [util.ArchitectureType.AARCH64],
+                    [],
+                ],
             ):
                 print(f, a)
-                path = os.path.join(dirname, "fakelib_good%s.dll" % f)
-                lib = util.LibraryPath(path)
+                path = tmp_path / f"fakelib_good_{f}.dll"
+                lib = util.LibraryPath(str(path))
                 assert lib.arch == a
-                if f != "_unknown":
-                    assert lib.is_32bit if 32 in a else not lib.is_32bit
-                    assert lib.is_64bit if 64 in a else not lib.is_64bit
-                    assert lib.bitness == ", ".join(str(b) for b in a)
-                else:
-                    assert lib.is_32bit == "n/a"
-                    assert lib.is_64bit == "n/a"
-                    assert lib.bitness == "n/a"
         finally:
             sys.platform = platform
 
@@ -601,19 +604,16 @@ class TestLibraryAnalysis(BaseTestCase):
             subprocess.run = alt_run
 
             for p, f, a in [
-                ("linux2", "32-bit", (32,)),
-                ("linux2", "32-bit & 64-bit", (32, 64)),
-                ("linux3", "64-bit", (64,)),
-                ("darwin", "(for architecture i386)", (32,)),
-                ("darwin", "(for architecture x86_64)", (64,)),
+                ("linux", "80386", [(util.ArchitectureType.I386)]),
+                ("linux", "x86-64", [(util.ArchitectureType.X86_64)]),
+                ("linux", "aarch64", [(util.ArchitectureType.AARCH64)]),
+                ("darwin", "executable i386", [(util.ArchitectureType.I386)]),
+                ("darwin", "executable x86_64", [(util.ArchitectureType.X86_64)]),
+                ("darwin", "executable arm64", [(util.ArchitectureType.AARCH64)]),
             ]:
                 sys.platform = p
                 lib = util.LibraryPath(f)
                 assert lib.arch == a
-                assert lib.is_32bit if 32 in a else not lib.is_32bit
-                assert lib.is_64bit if 64 in a else not lib.is_64bit
-                assert lib.bitness == ", ".join(str(b) for b in a)
-
         finally:
             sys.platform = platform
             subprocess.run = run
@@ -625,10 +625,7 @@ class TestLibraryAnalysis(BaseTestCase):
         try:
             sys.platform = "darwin"
             lib = util.LibraryPath("")
-            assert lib.arch == ()
-            assert lib.is_32bit == "n/a"
-            assert lib.is_64bit == "n/a"
-            assert lib.bitness == "n/a"
+            assert lib.arch == []
         finally:
             sys.platform = platform
             subprocess.run = run
@@ -640,10 +637,7 @@ class TestLibraryAnalysis(BaseTestCase):
         try:
             sys.platform = "test"
             lib = util.LibraryPath("")
-            assert lib.arch == ()
-            assert lib.is_32bit == "n/a"
-            assert lib.is_64bit == "n/a"
-            assert lib.bitness == "n/a"
+            assert lib.arch == []
         finally:
             sys.platform = platform
             subprocess.run = run