blob: c5426acb1dd5f86e2c64f163f1838ca756268acc [file] [edit]
#!/usr/bin/env python3
# Copyright (c) 2009 Giampaolo Rodola'. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Cross-platform lib for process and system monitoring in Python."""
import concurrent.futures
import glob
import os
import pathlib
import shlex
import shutil
import struct
import subprocess
import sys
import sysconfig
import tempfile
from setuptools import Extension
from setuptools import setup
from setuptools.command.build_ext import build_ext
ROOT_DIR = pathlib.Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT_DIR))
from _bootstrap import get_version # noqa: E402
from _bootstrap import load_module # noqa: E402
_common = load_module(ROOT_DIR / "psutil" / "_common.py")
AIX = _common.AIX
BSD = _common.BSD
FREEBSD = _common.FREEBSD
LINUX = _common.LINUX
MACOS = _common.MACOS
NETBSD = _common.NETBSD
OPENBSD = _common.OPENBSD
POSIX = _common.POSIX
SUNOS = _common.SUNOS
WINDOWS = _common.WINDOWS
hilite = _common.hilite
PYPY = '__pypy__' in sys.builtin_module_names
CPYTHON = sys.implementation.name == "cpython"
Py_GIL_DISABLED = sysconfig.get_config_var("Py_GIL_DISABLED")
# The pre-processor macros that are passed to the C compiler when
# building the extension.
macros = []
if POSIX:
macros.append(("PSUTIL_POSIX", 1))
if BSD:
macros.append(("PSUTIL_BSD", 1))
# Needed to determine _Py_PARSE_PID in case it's missing (PyPy).
# Taken from Lib/test/test_fcntl.py.
# XXX: not bullet proof as the (long long) case is missing.
if struct.calcsize('l') <= 8:
macros.append(('PSUTIL_SIZEOF_PID_T', '4')) # int
else:
macros.append(('PSUTIL_SIZEOF_PID_T', '8')) # long
sources = glob.glob("psutil/arch/all/*.c")
if POSIX:
sources.extend(glob.glob("psutil/arch/posix/*.c"))
VERSION = get_version()
macros.append(('PSUTIL_VERSION', int(VERSION.replace('.', ''))))
# The oldest interpreter we support, and the one the wheel claims to
# run on. Py_LIMITED_API lets us create a single wheel which works with
# multiple python versions, including unreleased ones.
MIN_PY_VERSION = (3, 8)
abi3_platform = MACOS or LINUX or WINDOWS # the ones we ship wheels for
if CPYTHON and abi3_platform and not Py_GIL_DISABLED:
_abi3_tag = "cp{}{}".format(*MIN_PY_VERSION)
_hexversion = "0x{:02x}{:02x}0000".format(*MIN_PY_VERSION)
py_limited_api = {"py_limited_api": True}
options = {"bdist_wheel": {"py_limited_api": _abi3_tag}}
macros.append(('Py_LIMITED_API', _hexversion))
else:
py_limited_api = {}
options = {}
def get_long_description():
script = ROOT_DIR / "scripts" / "internal" / "convert_readme.py"
readme = ROOT_DIR / 'README.rst'
p = subprocess.Popen(
[sys.executable, script, readme],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
stdout, stderr = p.communicate()
if p.returncode != 0:
raise RuntimeError(stderr)
return stdout
def num_cpus():
value = os.getenv("PSUTIL_BUILD_JOBS")
if value is not None:
return max(1, int(value))
fun = getattr(os, "process_cpu_count", os.cpu_count)
return fun() or 1
def has_python_h():
"""Whether a C file including Python.h really compiles."""
paths = sysconfig.get_paths()
incdirs = [paths["include"]]
if paths.get("platinclude") and paths["platinclude"] not in incdirs:
incdirs.append(paths["platinclude"])
args = []
for d in incdirs:
args.extend(["-I", d])
return unix_can_compile("#include <Python.h>", args)
def get_cc():
"""The compiler (plus flags) python uses to build C extensions."""
cc = os.getenv('CC') or sysconfig.get_config_var("CC") or "cc"
return shlex.split(cc)
def has_compiler():
return unix_can_compile("int main(void) { return 0; }")
def unix_can_compile(c_code, extra_args=()):
# https://github.com/giampaolo/psutil/pull/1568
with tempfile.TemporaryDirectory() as tempdir:
src = os.path.join(tempdir, "test.c")
with open(src, "w") as f:
f.write(c_code)
cmd = (
get_cc()
+ list(extra_args)
+ [
"-c",
src,
"-o",
os.path.join(tempdir, "test.o"),
]
)
try:
ret = subprocess.call(
cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
except OSError:
return False # compiler is not installed
return ret == 0
if WINDOWS:
def get_winver():
maj, min = sys.getwindowsversion()[0:2]
return "0x0{}".format((maj * 100) + min)
if sys.getwindowsversion()[0] < 10:
msg = "this Windows version is too old (< Windows 10); "
msg += "psutil 7.2.x is the latest version which supports Windows "
msg += "Vista, 7, 8, 8.1 and their server counterparts"
raise RuntimeError(msg)
macros.append(("PSUTIL_WINDOWS", 1))
macros.extend([
# be nice to mingw, see:
# http://www.mingw.org/wiki/Use_more_recent_defined_functions
('_WIN32_WINNT', get_winver()),
('_AVAIL_WINVER_', get_winver()),
('_CRT_SECURE_NO_WARNINGS', None),
])
if Py_GIL_DISABLED:
macros.append(('Py_GIL_DISABLED', 1))
ext = Extension(
'psutil._psutil',
sources=(
sources
+ ["psutil/_psutil_windows.c"]
+ glob.glob("psutil/arch/windows/*.c")
),
define_macros=macros,
libraries=[
"advapi32",
"iphlpapi",
"kernel32",
"netapi32",
"ntdll",
"pdh",
"PowrProf",
"shell32",
"ws2_32",
],
# extra_compile_args=["/W 4"],
# extra_link_args=["/DEBUG"],
**py_limited_api,
)
elif MACOS:
macros.extend([("PSUTIL_OSX", 1), ("PSUTIL_MACOS", 1)])
ext = Extension(
'psutil._psutil',
sources=(
sources
+ ["psutil/_psutil_osx.c"]
+ glob.glob("psutil/arch/osx/*.c")
),
define_macros=macros,
extra_link_args=[
'-framework',
'CoreFoundation',
'-framework',
'IOKit',
],
**py_limited_api,
)
elif FREEBSD:
macros.append(("PSUTIL_FREEBSD", 1))
ext = Extension(
'psutil._psutil',
sources=(
sources
+ ["psutil/_psutil_bsd.c"]
+ glob.glob("psutil/arch/bsd/*.c")
+ glob.glob("psutil/arch/freebsd/*.c")
),
define_macros=macros,
libraries=["devstat"],
**py_limited_api,
)
elif OPENBSD:
macros.append(("PSUTIL_OPENBSD", 1))
ext = Extension(
'psutil._psutil',
sources=(
sources
+ ["psutil/_psutil_bsd.c"]
+ glob.glob("psutil/arch/bsd/*.c")
+ glob.glob("psutil/arch/openbsd/*.c")
),
define_macros=macros,
libraries=["kvm"],
**py_limited_api,
)
elif NETBSD:
macros.append(("PSUTIL_NETBSD", 1))
ext = Extension(
'psutil._psutil',
sources=(
sources
+ ["psutil/_psutil_bsd.c"]
+ glob.glob("psutil/arch/bsd/*.c")
+ glob.glob("psutil/arch/netbsd/*.c")
),
define_macros=macros,
libraries=["kvm", "jemalloc"],
**py_limited_api,
)
elif LINUX:
# see: https://github.com/giampaolo/psutil/issues/659
if not unix_can_compile("#include <linux/ethtool.h>"):
macros.append(("PSUTIL_ETHTOOL_MISSING_TYPES", 1))
macros.append(("PSUTIL_LINUX", 1))
ext = Extension(
'psutil._psutil',
sources=(
sources
+ ["psutil/_psutil_linux.c"]
+ glob.glob("psutil/arch/linux/*.c")
),
define_macros=macros,
**py_limited_api,
)
elif SUNOS:
macros.append(("PSUTIL_SUNOS", 1))
ext = Extension(
'psutil._psutil',
sources=(
sources
+ ["psutil/_psutil_sunos.c"]
+ glob.glob("psutil/arch/sunos/*.c")
),
define_macros=macros,
libraries=["kstat", "nsl", "socket"],
**py_limited_api,
)
elif AIX:
macros.append(("PSUTIL_AIX", 1))
ext = Extension(
'psutil._psutil',
sources=(
sources
+ ["psutil/_psutil_aix.c"]
+ glob.glob("psutil/arch/aix/*.c")
),
libraries=["perfstat"],
define_macros=macros,
**py_limited_api,
)
else:
sys.exit("platform {} is not supported".format(sys.platform))
class BuildExt(build_ext):
"""Compile the C sources in parallel."""
def build_extensions(self): # override
compiler = self.compiler
real_spawn = compiler.spawn
real_compile = compiler.compile
def parallel_compile(*args, **kwargs):
# Run compile() as usual, but have every compiler
# invocation return right away, then wait for all of them.
# Hooking spawn() instead of the private per-file methods
# is what makes this work on Windows as well.
futures = []
with concurrent.futures.ThreadPoolExecutor(num_cpus()) as pool:
compiler.spawn = lambda cmd, **kw: futures.append(
pool.submit(real_spawn, cmd, **kw)
)
try:
objects = real_compile(*args, **kwargs)
finally:
compiler.spawn = real_spawn
for fut in concurrent.futures.as_completed(futures):
fut.result() # let compiler errors surface
return objects
compiler.compile = parallel_compile
super().build_extensions()
def print_install_instructions():
def install_sysdeps_cmd():
url = (
"https://raw.githubusercontent.com/giampaolo/psutil/"
"master/scripts/internal/install-sysdeps.sh"
)
if shutil.which("curl"):
return f"curl -fsSL {url} | sh"
if shutil.which("wget"):
return f"wget -qO- {url} | sh"
if shutil.which("fetch"): # FreeBSD
return f"fetch -qo - {url} | sh"
if shutil.which("ftp"): # OpenBSD / NetBSD
return f"ftp -o - {url} | sh"
if not has_compiler():
suggest = "A working C compiler is not installed."
if MACOS:
cmd = "xcode-select --install"
elif AIX or PYPY:
cmd = None
else:
cmd = install_sysdeps_cmd()
elif not has_python_h():
suggest = "Python header files are not installed."
if MACOS or AIX or PYPY: # noqa: SIM108
cmd = None
else:
cmd = install_sysdeps_cmd()
else:
return
if cmd:
suggest += f" Try running:\n{cmd}"
print(hilite(suggest, color="red", bold=True), file=sys.stderr)
def main():
kwargs = dict(
name='psutil',
version=VERSION,
description="Cross-platform lib for process and system monitoring.",
long_description=get_long_description(),
long_description_content_type='text/x-rst',
# fmt: off
keywords=[
'ps', 'top', 'kill', 'free', 'lsof', 'netstat', 'df', 'uptime',
'taskmgr', 'process', 'monitoring', 'performance', 'metrics',
'observability',
],
# fmt: on
author='Giampaolo Rodola',
author_email='g.rodola@gmail.com',
url='https://github.com/giampaolo/psutil',
platforms='Platform Independent',
license='BSD-3-Clause',
packages=['psutil'],
ext_modules=[ext],
cmdclass={'build_ext': BuildExt if num_cpus() > 1 else build_ext},
options=options,
python_requires=">={}.{}".format(*MIN_PY_VERSION),
# https://docs.pypi.org/project_metadata/
project_urls={
'Homepage': 'https://github.com/giampaolo/psutil',
'Source': 'https://github.com/giampaolo/psutil',
'Issues': 'https://github.com/giampaolo/psutil/issues',
'Documentation': 'https://psutil.io/',
'Changelog': 'https://psutil.io/changelog/',
'Funding': 'https://github.com/sponsors/giampaolo',
},
# https://pypi.org/classifiers/
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Operating System :: MacOS :: MacOS X',
'Operating System :: Microsoft :: Windows',
'Operating System :: Microsoft :: Windows :: Windows 10',
'Operating System :: Microsoft :: Windows :: Windows 11',
'Operating System :: POSIX :: AIX',
'Operating System :: POSIX :: BSD',
'Operating System :: POSIX :: BSD :: FreeBSD',
'Operating System :: POSIX :: BSD :: NetBSD',
'Operating System :: POSIX :: BSD :: OpenBSD',
'Operating System :: POSIX :: Linux',
'Operating System :: POSIX :: SunOS/Solaris',
'Operating System :: POSIX',
'Programming Language :: C',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Programming Language :: Python :: Free Threading',
'Topic :: Software Development :: Libraries',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Benchmark',
'Topic :: System :: Hardware',
'Topic :: System :: Monitoring',
'Topic :: System :: Networking :: Monitoring',
'Topic :: System :: Networking :: Monitoring :: Hardware Watchdog',
'Topic :: System :: Networking',
'Topic :: System :: Operating System',
'Topic :: System :: Systems Administration',
'Topic :: Utilities',
],
)
success = False
try:
setup(**kwargs)
success = True
finally:
cmd = sys.argv[1] if len(sys.argv) >= 2 else ''
if (
not success
and POSIX
and cmd.startswith(("build", "install", "bdist", "develop"))
):
print_install_instructions()
if __name__ == '__main__':
main()