diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4e9c745e..d23bd179c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,23 +18,20 @@ concurrency: jobs: # Linux + macOS + Windows Python 3 py3: - name: py3-${{ matrix.os }}-${{ startsWith(matrix.os, 'ubuntu') && 'all' || matrix.archs }} + name: "py3-${{ matrix.os }}-${{ matrix.arch }}" runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: fail-fast: false matrix: include: - - os: ubuntu-latest - archs: "x86_64 i686" - - os: macos-12 - archs: "x86_64" - - os: macos-14 - archs: "arm64" - - os: windows-2019 - archs: "AMD64" - - os: windows-2019 - archs: "x86" + - {os: ubuntu-latest, arch: x86_64} + - {os: ubuntu-latest, arch: i686} + - {os: ubuntu-latest, arch: aarch64} + - {os: macos-12, arch: x86_64} + - {os: macos-14, arch: arm64} + - {os: windows-2019, arch: AMD64} + - {os: windows-2019, arch: x86} steps: - uses: actions/checkout@v4 @@ -51,16 +48,20 @@ jobs: with: python-version: 3.11 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + if: matrix.arch == 'aarch64' + - name: Create wheels + run tests - uses: pypa/cibuildwheel@v2.18.0 + uses: pypa/cibuildwheel@v2.19.1 env: - CIBW_ARCHS: "${{ matrix.archs }}" + CIBW_ARCHS: "${{ matrix.arch }}" CIBW_PRERELEASE_PYTHONS: True - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: wheels-py3-${{ matrix.os }}-${{ startsWith(matrix.os, 'ubuntu') && 'all' || matrix.archs }} + name: wheels-py3-${{ matrix.os }}-${{ matrix.arch }} path: wheelhouse - name: Generate .tar.gz diff --git a/HISTORY.rst b/HISTORY.rst index ae664900f..5107b1345 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -18,6 +18,7 @@ - 2407_: `Process.connections()`_ was renamed to `Process.net_connections()`_. The old name is still available, but it's deprecated (triggers a ``DeprecationWarning``) and will be removed in the future. +- 2425_: [Linux]: provide aarch64 wheels. (patch by Matthieu Darbois / Ben Raz) **Bug fixes** diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 684c68ae2..dccb9e4b2 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -87,7 +87,7 @@ "HAS_IONICE", "HAS_MEMORY_MAPS", "HAS_PROC_CPU_NUM", "HAS_RLIMIT", "HAS_SENSORS_BATTERY", "HAS_BATTERY", "HAS_SENSORS_FANS", "HAS_SENSORS_TEMPERATURES", "HAS_NET_CONNECTIONS_UNIX", "MACOS_11PLUS", - "MACOS_12PLUS", "COVERAGE", + "MACOS_12PLUS", "COVERAGE", 'AARCH64', "QEMU_USER", # subprocesses 'pyrun', 'terminate', 'reap_children', 'spawn_testproc', 'spawn_zombie', 'spawn_children_pair', @@ -128,8 +128,14 @@ GITHUB_ACTIONS = 'GITHUB_ACTIONS' in os.environ or 'CIBUILDWHEEL' in os.environ CI_TESTING = APPVEYOR or GITHUB_ACTIONS COVERAGE = 'COVERAGE_RUN' in os.environ +if LINUX and GITHUB_ACTIONS: + with open('/proc/1/cmdline') as f: + QEMU_USER = "/bin/qemu-" in f.read() +else: + QEMU_USER = False # are we a 64 bit process? IS_64BIT = sys.maxsize > 2**32 +AARCH64 = platform.machine() == "aarch64" @memoize diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index e768e7d52..a5469ac8a 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -30,6 +30,7 @@ from psutil.tests import HAS_SENSORS_FANS from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import PYPY +from psutil.tests import QEMU_USER from psutil.tests import SKIP_SYSCONS from psutil.tests import PsutilTestCase from psutil.tests import create_sockets @@ -277,6 +278,7 @@ def test_net_if_addrs(self): self.assertIsInstance(addr.netmask, (str, type(None))) self.assertIsInstance(addr.broadcast, (str, type(None))) + @unittest.skipIf(QEMU_USER, 'QEMU user not supported') def test_net_if_stats(self): # Duplicate of test_system.py. Keep it anyway. for ifname, info in psutil.net_if_stats().items(): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index be264ae1d..d8faac968 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -28,6 +28,7 @@ from psutil._compat import PY3 from psutil._compat import FileNotFoundError from psutil._compat import basestring +from psutil.tests import AARCH64 from psutil.tests import GITHUB_ACTIONS from psutil.tests import GLOBAL_TIMEOUT from psutil.tests import HAS_BATTERY @@ -35,6 +36,7 @@ from psutil.tests import HAS_GETLOADAVG from psutil.tests import HAS_RLIMIT from psutil.tests import PYPY +from psutil.tests import QEMU_USER from psutil.tests import TOLERANCE_DISK_USAGE from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import PsutilTestCase @@ -277,8 +279,14 @@ def test_used(self): # This got changed in: # https://gitlab.com/procps-ng/procps/commit/ # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e + # Newer versions of procps are using yet another way to compute used + # memory. + # https://gitlab.com/procps-ng/procps/commit/ + # 2184e90d2e7cdb582f9a5b706b47015e56707e4d if get_free_version_info() < (3, 3, 12): - raise unittest.SkipTest("old free version") + raise unittest.SkipTest("free version too old") + if get_free_version_info() >= (4, 0, 0): + raise unittest.SkipTest("free version too recent") cli_value = free_physmem().used psutil_value = psutil.virtual_memory().used self.assertAlmostEqual( @@ -341,8 +349,14 @@ def test_used(self): # This got changed in: # https://gitlab.com/procps-ng/procps/commit/ # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e + # Newer versions of procps are using yet another way to compute used + # memory. + # https://gitlab.com/procps-ng/procps/commit/ + # 2184e90d2e7cdb582f9a5b706b47015e56707e4d if get_free_version_info() < (3, 3, 12): - raise unittest.SkipTest("old free version") + raise unittest.SkipTest("free version too old") + if get_free_version_info() >= (4, 0, 0): + raise unittest.SkipTest("free version too recent") vmstat_value = vmstat('used memory') * 1024 psutil_value = psutil.virtual_memory().used self.assertAlmostEqual( @@ -830,6 +844,7 @@ def path_exists_mock(path): assert psutil.cpu_freq() @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + @unittest.skipIf(AARCH64, "aarch64 does not report mhz in /proc/cpuinfo") def test_emulate_use_cpuinfo(self): # Emulate a case where /sys/devices/system/cpu/cpufreq* does not # exist and /proc/cpuinfo is used instead. @@ -1037,6 +1052,7 @@ def test_ips(self): @unittest.skipIf(not LINUX, "LINUX only") +@unittest.skipIf(QEMU_USER, "QEMU user not supported") class TestSystemNetIfStats(PsutilTestCase): @unittest.skipIf(not which("ifconfig"), "ifconfig utility not available") def test_against_ifconfig(self): @@ -1596,7 +1612,7 @@ def test_issue_687(self): with ThreadTask(): p = psutil.Process() threads = p.threads() - self.assertEqual(len(threads), 2) + self.assertEqual(len(threads), 3 if QEMU_USER else 2) tid = sorted(threads, key=lambda x: x.id)[1].id self.assertNotEqual(p.pid, tid) pt = psutil.Process(tid) @@ -2276,6 +2292,7 @@ def test_name(self): value = self.read_status_file("Name:") self.assertEqual(self.proc.name(), value) + @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_status(self): value = self.read_status_file("State:") value = value[value.find('(') + 1 : value.rfind(')')] diff --git a/psutil/tests/test_memleaks.py b/psutil/tests/test_memleaks.py index 8232b18ad..6506497c9 100755 --- a/psutil/tests/test_memleaks.py +++ b/psutil/tests/test_memleaks.py @@ -19,6 +19,7 @@ import functools import os import platform +import sys import unittest import psutil @@ -43,6 +44,7 @@ from psutil.tests import HAS_SENSORS_BATTERY from psutil.tests import HAS_SENSORS_FANS from psutil.tests import HAS_SENSORS_TEMPERATURES +from psutil.tests import QEMU_USER from psutil.tests import TestMemoryLeak from psutil.tests import create_sockets from psutil.tests import get_testfn @@ -398,6 +400,7 @@ def test_disk_usage(self): times = FEW_TIMES if POSIX else self.times self.execute(lambda: psutil.disk_usage('.'), times=times) + @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_disk_partitions(self): self.execute(psutil.disk_partitions) @@ -435,6 +438,7 @@ def test_net_if_addrs(self): tolerance = 80 * 1024 if WINDOWS else self.tolerance self.execute(psutil.net_if_addrs, tolerance=tolerance) + @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_net_if_stats(self): self.execute(psutil.net_if_stats) @@ -491,6 +495,11 @@ def test_win_service_get_description(self): if __name__ == '__main__': + from psutil.tests.runner import cprint from psutil.tests.runner import run_from_name + if QEMU_USER: + cprint("skipping %s tests under QEMU_USER" % __file__, "brown") + sys.exit(0) + run_from_name(__file__) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 59416592e..5c05d1764 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -43,6 +43,7 @@ from psutil.tests import HAS_SENSORS_TEMPERATURES from psutil.tests import PYTHON_EXE from psutil.tests import PYTHON_EXE_ENV +from psutil.tests import QEMU_USER from psutil.tests import SCRIPTS_DIR from psutil.tests import PsutilTestCase from psutil.tests import mock @@ -288,6 +289,9 @@ def check(ret): for fun, name in ns.iter(ns.getters): if name in {"win_service_iter", "win_service_get"}: continue + if QEMU_USER and name == "net_if_stats": + # OSError: [Errno 38] ioctl(SIOCETHTOOL) not implemented + continue with self.subTest(name=name): try: ret = fun() @@ -1008,6 +1012,7 @@ def test_pstree(self): def test_netstat(self): self.assert_stdout('netstat.py') + @unittest.skipIf(QEMU_USER, 'QEMU user not supported') def test_ifconfig(self): self.assert_stdout('ifconfig.py') diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index 5203c2707..941f0fac1 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -25,6 +25,7 @@ from psutil import SUNOS from psutil.tests import HAS_NET_IO_COUNTERS from psutil.tests import PYTHON_EXE +from psutil.tests import QEMU_USER from psutil.tests import PsutilTestCase from psutil.tests import mock from psutil.tests import retry_on_failure @@ -102,7 +103,11 @@ def ps_name(pid): field = "command" if SUNOS: field = "comm" - return ps(field, pid).split()[0] + command = ps(field, pid).split() + if QEMU_USER: + assert "/bin/qemu-" in command[0] + return command[1] + return command[0] def ps_args(pid): diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index 1ee5393c2..363474c78 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -54,6 +54,7 @@ from psutil.tests import PYPY from psutil.tests import PYTHON_EXE from psutil.tests import PYTHON_EXE_ENV +from psutil.tests import QEMU_USER from psutil.tests import PsutilTestCase from psutil.tests import ThreadTask from psutil.tests import call_until @@ -253,6 +254,7 @@ def test_cpu_percent_numcpus_none(self): psutil.Process().cpu_percent() assert m.called + @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_cpu_times(self): times = psutil.Process().cpu_times() assert times.user >= 0.0, times @@ -265,6 +267,7 @@ def test_cpu_times(self): for name in times._fields: time.strftime("%H:%M:%S", time.localtime(getattr(times, name))) + @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_cpu_times_2(self): user_time, kernel_time = psutil.Process().cpu_times()[:2] utime, ktime = os.times()[:2] @@ -633,6 +636,8 @@ def test_memory_maps(self): for nt in maps: if not nt.path.startswith('['): + if QEMU_USER and "/bin/qemu-" in nt.path: + continue assert os.path.isabs(nt.path), nt.path if POSIX: try: @@ -698,6 +703,7 @@ def test_is_running(self): assert not p.is_running() assert not p.is_running() + @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_exe(self): p = self.spawn_psproc() exe = p.exe() @@ -754,6 +760,9 @@ def test_cmdline(self): ' '.join(p.cmdline()[1:]), ' '.join(cmdline[1:]) ) return + if QEMU_USER: + self.assertEqual(' '.join(p.cmdline()[2:]), ' '.join(cmdline)) + return self.assertEqual(' '.join(p.cmdline()), ' '.join(cmdline)) @unittest.skipIf(PYPY, "broken on PYPY") @@ -771,13 +780,14 @@ def test_long_cmdline(self): self.assertEqual(p.cmdline(), cmdline) except psutil.ZombieProcess: raise unittest.SkipTest("OPENBSD: process turned into zombie") - elif NETBSD: + elif QEMU_USER: + self.assertEqual(p.cmdline()[2:], cmdline) + else: ret = p.cmdline() - if ret == []: + if NETBSD and ret == []: # https://github.com/giampaolo/psutil/issues/2250 raise unittest.SkipTest("OPENBSD: returned EBUSY") - - self.assertEqual(p.cmdline(), cmdline) + self.assertEqual(ret, cmdline) def test_name(self): p = self.spawn_psproc() @@ -785,7 +795,8 @@ def test_name(self): pyexe = os.path.basename(os.path.realpath(sys.executable)).lower() assert pyexe.startswith(name), (pyexe, name) - @unittest.skipIf(PYPY, "unreliable on PYPY") + @unittest.skipIf(PYPY or QEMU_USER, "unreliable on PYPY") + @unittest.skipIf(QEMU_USER, "unreliable on QEMU user") def test_long_name(self): pyexe = create_py_exe(self.get_testfn(suffix="0123456789" * 2)) cmdline = [ @@ -816,6 +827,7 @@ def test_long_name(self): @unittest.skipIf(SUNOS, "broken on SUNOS") @unittest.skipIf(AIX, "broken on AIX") @unittest.skipIf(PYPY, "broken on PYPY") + @unittest.skipIf(QEMU_USER, "broken on QEMU user") def test_prog_w_funky_name(self): # Test that name(), exe() and cmdline() correctly handle programs # with funky chars such as spaces and ")", see: @@ -922,6 +934,7 @@ def cleanup(init): except psutil.AccessDenied: pass + @unittest.skipIf(QEMU_USER, "QEMU user not supported") def test_status(self): p = psutil.Process() self.assertEqual(p.status(), psutil.STATUS_RUNNING) @@ -1149,6 +1162,7 @@ def test_parent_multi(self): self.assertEqual(grandchild.parent(), child) self.assertEqual(child.parent(), parent) + @unittest.skipIf(QEMU_USER, "QEMU user not supported") @retry_on_failure() def test_parents(self): parent = psutil.Process() diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index d1f476bb5..48833a105 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -32,6 +32,7 @@ from psutil._compat import long from psutil._compat import unicode from psutil.tests import CI_TESTING +from psutil.tests import QEMU_USER from psutil.tests import VALID_PROC_STATUSES from psutil.tests import PsutilTestCase from psutil.tests import check_connection_ntuple @@ -235,6 +236,9 @@ def username(self, ret, info): def status(self, ret, info): self.assertIsInstance(ret, str) assert ret, ret + if QEMU_USER: + # status does not work under qemu user + return self.assertNotEqual(ret, '?') # XXX self.assertIn(ret, VALID_PROC_STATUSES) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 554fbffb2..e228f6d32 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -48,6 +48,7 @@ from psutil.tests import IS_64BIT from psutil.tests import MACOS_12PLUS from psutil.tests import PYPY +from psutil.tests import QEMU_USER from psutil.tests import UNICODE_SUFFIX from psutil.tests import PsutilTestCase from psutil.tests import check_net_address @@ -806,6 +807,7 @@ def test_net_io_counters_no_nics(self): self.assertEqual(psutil.net_io_counters(pernic=True), {}) assert m.called + @unittest.skipIf(QEMU_USER, 'QEMU user not supported') def test_net_if_addrs(self): nics = psutil.net_if_addrs() assert nics, nics @@ -893,6 +895,7 @@ def test_net_if_addrs_mac_null_bytes(self): else: self.assertEqual(addr.address, '06-3d-29-00-00-00') + @unittest.skipIf(QEMU_USER, 'QEMU user not supported') def test_net_if_stats(self): nics = psutil.net_if_stats() assert nics, nics diff --git a/pyproject.toml b/pyproject.toml index b03392071..ba13f2829 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -207,6 +207,7 @@ trailing_comma_inline_array = true skip = [ "*-musllinux*", "cp313-win*", # pywin32 is not available on cp313 yet + "cp3{7,8,9,10,11,12}-*linux_{aarch64,ppc64le,s390x}", # Only test cp36/cp313 on qemu tested architectures "pp*", ] test-command = [ @@ -218,6 +219,9 @@ test-extras = "test" [tool.cibuildwheel.macos] archs = ["arm64", "x86_64"] +[tool.cibuildwheel.linux] +before-all = "yum install -y net-tools" + [build-system] build-backend = "setuptools.build_meta" requires = ["setuptools>=43", "wheel"]