Repository: easybuilders/easybuild-framework

Issue:
--show-system-info fails on Apple Silicon M1
Different system queries via `sysctl` are broken on Apple Silicon.

Known issues:

* "`sysctl -n hw.cpufrequency_max`" returns empty result, used in `get_cpu_speed`
  * see also https://github.com/sebhildebrandt/systeminformation/issues/564)
* `machdep.cpu.vendor` (used in `get_cpu_vendor`) and `machdep.cpu.extfeatures` (used in `get_cpu_features`) are not supported, triggers `sysctl: unknown oid` error


Discussion:

Resolving patch:
diff --git a/easybuild/tools/systemtools.py b/easybuild/tools/systemtools.py
--- a/easybuild/tools/systemtools.py
+++ b/easybuild/tools/systemtools.py
@@ -94,6 +94,7 @@
 # Vendor constants
 AMD = 'AMD'
 APM = 'Applied Micro'
+APPLE = 'Apple'
 ARM = 'ARM'
 BROADCOM = 'Broadcom'
 CAVIUM = 'Cavium'
@@ -123,7 +124,7 @@
 
 CPU_ARCHITECTURES = [AARCH32, AARCH64, POWER, RISCV32, RISCV64, X86_64]
 CPU_FAMILIES = [AMD, ARM, INTEL, POWER, POWER_LE, RISCV]
-CPU_VENDORS = [AMD, APM, ARM, BROADCOM, CAVIUM, DEC, IBM, INTEL, MARVELL, MOTOROLA, NVIDIA, QUALCOMM]
+CPU_VENDORS = [AMD, APM, APPLE, ARM, BROADCOM, CAVIUM, DEC, IBM, INTEL, MARVELL, MOTOROLA, NVIDIA, QUALCOMM]
 # ARM implementer IDs (i.e., the hexadeximal keys) taken from ARMv8-A Architecture Reference Manual
 # (ARM DDI 0487A.j, Section G6.2.102, Page G6-4493)
 VENDOR_IDS = {
@@ -384,11 +385,18 @@ def get_cpu_vendor():
 
     elif os_type == DARWIN:
         cmd = "sysctl -n machdep.cpu.vendor"
-        out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False)
+        out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, log_ok=False)
         out = out.strip()
         if ec == 0 and out in VENDOR_IDS:
             vendor = VENDOR_IDS[out]
             _log.debug("Determined CPU vendor on DARWIN as being '%s' via cmd '%s" % (vendor, cmd))
+        else:
+            cmd = "sysctl -n machdep.cpu.brand_string"
+            out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, log_ok=False)
+            out = out.strip().split(' ')[0]
+            if ec == 0 and out in CPU_VENDORS:
+                vendor = out
+                _log.debug("Determined CPU vendor on DARWIN as being '%s' via cmd '%s" % (vendor, cmd))
 
     if vendor is None:
         vendor = UNKNOWN
@@ -533,9 +541,11 @@ def get_cpu_speed():
         cmd = "sysctl -n hw.cpufrequency_max"
         _log.debug("Trying to determine CPU frequency on Darwin via cmd '%s'" % cmd)
         out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False)
-        if ec == 0:
+        out = out.strip()
+        cpu_freq = None
+        if ec == 0 and out:
             # returns clock frequency in cycles/sec, but we want MHz
-            cpu_freq = float(out.strip()) // (1000 ** 2)
+            cpu_freq = float(out) // (1000 ** 2)
 
     else:
         raise SystemToolsException("Could not determine CPU clock frequency (OS: %s)." % os_type)
@@ -578,7 +588,7 @@ def get_cpu_features():
         for feature_set in ['extfeatures', 'features', 'leaf7_features']:
             cmd = "sysctl -n machdep.cpu.%s" % feature_set
             _log.debug("Trying to determine CPU features on Darwin via cmd '%s'", cmd)
-            out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False)
+            out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, log_ok=False)
             if ec == 0:
                 cpu_feat.extend(out.strip().lower().split())
 


Test patch:
diff --git a/test/framework/systemtools.py b/test/framework/systemtools.py
--- a/test/framework/systemtools.py
+++ b/test/framework/systemtools.py
@@ -495,7 +495,7 @@ def test_cpu_features_native(self):
         """Test getting CPU features."""
         cpu_feat = get_cpu_features()
         self.assertTrue(isinstance(cpu_feat, list))
-        self.assertTrue(len(cpu_feat) > 0)
+        self.assertTrue(len(cpu_feat) >= 0)
         self.assertTrue(all(isinstance(x, string_type) for x in cpu_feat))
 
     def test_cpu_features_linux(self):
@@ -1016,8 +1016,10 @@ def test_check_linked_shared_libs(self):
             self.assertEqual(check_linked_shared_libs(txt_path, **pattern_named_args), None)
             self.assertEqual(check_linked_shared_libs(broken_symlink_path, **pattern_named_args), None)
             for path in (bin_ls_path, lib_path):
-                error_msg = "Check on linked libs should pass for %s with %s" % (path, pattern_named_args)
-                self.assertTrue(check_linked_shared_libs(path, **pattern_named_args), error_msg)
+                # path may not exist, especially for library paths obtained via 'otool -L' on macOS
+                if os.path.exists(path):
+                    error_msg = "Check on linked libs should pass for %s with %s" % (path, pattern_named_args)
+                    self.assertTrue(check_linked_shared_libs(path, **pattern_named_args), error_msg)
 
         # also test with input that should result in failing check
         test_pattern_named_args = [
@@ -1050,9 +1052,10 @@ def test_locate_solib(self):
 
     def test_find_library_path(self):
         """Test find_library_path function (Linux and Darwin only)."""
-        if get_os_type() == LINUX:
+        os_type = get_os_type()
+        if os_type == LINUX:
             libname = 'libc.so.6'
-        elif get_os_type() == DARWIN:
+        elif os_type == DARWIN:
             libname = 'libSystem.dylib'
         else:
             libname = None
@@ -1060,7 +1063,7 @@ def test_find_library_path(self):
         if libname:
             lib_path = find_library_path(libname)
             self.assertEqual(os.path.basename(lib_path), libname)
-            self.assertTrue(os.path.exists(lib_path), "%s should exist" % libname)
+            self.assertTrue(os.path.exists(lib_path) or os_type == DARWIN, "%s should exist" % libname)
 
 
 def suite():