1#!/usr/bin/env python3 2 3""" This module tries to retrieve as much platform-identifying data as 4 possible. It makes this information available via function APIs. 5 6 If called from the command line, it prints the platform 7 information concatenated as single string to stdout. The output 8 format is usable as part of a filename. 9 10""" 11# This module is maintained by Marc-Andre Lemburg <[email protected]>. 12# If you find problems, please submit bug reports/patches via the 13# Python bug tracker (http://bugs.python.org) and assign them to "lemburg". 14# 15# Still needed: 16# * support for MS-DOS (PythonDX ?) 17# * support for Amiga and other still unsupported platforms running Python 18# * support for additional Linux distributions 19# 20# Many thanks to all those who helped adding platform-specific 21# checks (in no particular order): 22# 23# Charles G Waldman, David Arnold, Gordon McMillan, Ben Darnell, 24# Jeff Bauer, Cliff Crawford, Ivan Van Laningham, Josef 25# Betancourt, Randall Hopper, Karl Putland, John Farrell, Greg 26# Andruk, Just van Rossum, Thomas Heller, Mark R. Levinson, Mark 27# Hammond, Bill Tutt, Hans Nowak, Uwe Zessin (OpenVMS support), 28# Colin Kong, Trent Mick, Guido van Rossum, Anthony Baxter, Steve 29# Dower 30# 31# History: 32# 33# <see CVS and SVN checkin messages for history> 34# 35# 1.0.8 - changed Windows support to read version from kernel32.dll 36# 1.0.7 - added DEV_NULL 37# 1.0.6 - added linux_distribution() 38# 1.0.5 - fixed Java support to allow running the module on Jython 39# 1.0.4 - added IronPython support 40# 1.0.3 - added normalization of Windows system name 41# 1.0.2 - added more Windows support 42# 1.0.1 - reformatted to make doc.py happy 43# 1.0.0 - reformatted a bit and checked into Python CVS 44# 0.8.0 - added sys.version parser and various new access 45# APIs (python_version(), python_compiler(), etc.) 46# 0.7.2 - fixed architecture() to use sizeof(pointer) where available 47# 0.7.1 - added support for Caldera OpenLinux 48# 0.7.0 - some fixes for WinCE; untabified the source file 49# 0.6.2 - support for OpenVMS - requires version 1.5.2-V006 or higher and 50# vms_lib.getsyi() configured 51# 0.6.1 - added code to prevent 'uname -p' on platforms which are 52# known not to support it 53# 0.6.0 - fixed win32_ver() to hopefully work on Win95,98,NT and Win2k; 54# did some cleanup of the interfaces - some APIs have changed 55# 0.5.5 - fixed another type in the MacOS code... should have 56# used more coffee today ;-) 57# 0.5.4 - fixed a few typos in the MacOS code 58# 0.5.3 - added experimental MacOS support; added better popen() 59# workarounds in _syscmd_ver() -- still not 100% elegant 60# though 61# 0.5.2 - fixed uname() to return '' instead of 'unknown' in all 62# return values (the system uname command tends to return 63# 'unknown' instead of just leaving the field empty) 64# 0.5.1 - included code for slackware dist; added exception handlers 65# to cover up situations where platforms don't have os.popen 66# (e.g. Mac) or fail on socket.gethostname(); fixed libc 67# detection RE 68# 0.5.0 - changed the API names referring to system commands to *syscmd*; 69# added java_ver(); made syscmd_ver() a private 70# API (was system_ver() in previous versions) -- use uname() 71# instead; extended the win32_ver() to also return processor 72# type information 73# 0.4.0 - added win32_ver() and modified the platform() output for WinXX 74# 0.3.4 - fixed a bug in _follow_symlinks() 75# 0.3.3 - fixed popen() and "file" command invocation bugs 76# 0.3.2 - added architecture() API and support for it in platform() 77# 0.3.1 - fixed syscmd_ver() RE to support Windows NT 78# 0.3.0 - added system alias support 79# 0.2.3 - removed 'wince' again... oh well. 80# 0.2.2 - added 'wince' to syscmd_ver() supported platforms 81# 0.2.1 - added cache logic and changed the platform string format 82# 0.2.0 - changed the API to use functions instead of module globals 83# since some action take too long to be run on module import 84# 0.1.0 - first release 85# 86# You can always get the latest version of this module at: 87# 88# http://www.egenix.com/files/python/platform.py 89# 90# If that URL should fail, try contacting the author. 91 92__copyright__ = """ 93 Copyright (c) 1999-2000, Marc-Andre Lemburg; mailto:[email protected] 94 Copyright (c) 2000-2010, eGenix.com Software GmbH; mailto:[email protected] 95 96 Permission to use, copy, modify, and distribute this software and its 97 documentation for any purpose and without fee or royalty is hereby granted, 98 provided that the above copyright notice appear in all copies and that 99 both that copyright notice and this permission notice appear in 100 supporting documentation or portions thereof, including modifications, 101 that you make. 102 103 EGENIX.COM SOFTWARE GMBH DISCLAIMS ALL WARRANTIES WITH REGARD TO 104 THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 105 FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, 106 INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING 107 FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, 108 NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION 109 WITH THE USE OR PERFORMANCE OF THIS SOFTWARE ! 110 111""" 112 113__version__ = '1.0.8' 114 115import collections 116import os 117import re 118import sys 119import functools 120import itertools 121 122### Globals & Constants 123 124# Helper for comparing two version number strings. 125# Based on the description of the PHP's version_compare(): 126# http://php.net/manual/en/function.version-compare.php 127 128_ver_stages = { 129 # any string not found in this dict, will get 0 assigned 130 'dev': 10, 131 'alpha': 20, 'a': 20, 132 'beta': 30, 'b': 30, 133 'c': 40, 134 'RC': 50, 'rc': 50, 135 # number, will get 100 assigned 136 'pl': 200, 'p': 200, 137} 138 139_component_re = re.compile(r'([0-9]+|[._+-])') 140 141def _comparable_version(version): 142 result = [] 143 for v in _component_re.split(version): 144 if v not in '._+-': 145 try: 146 v = int(v, 10) 147 t = 100 148 except ValueError: 149 t = _ver_stages.get(v, 0) 150 result.extend((t, v)) 151 return result 152 153### Platform specific APIs 154 155_libc_search = re.compile(b'(__libc_init)' 156 b'|' 157 b'(GLIBC_([0-9.]+))' 158 b'|' 159 br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)', re.ASCII) 160 161def libc_ver(executable=None, lib='', version='', chunksize=16384): 162 163 """ Tries to determine the libc version that the file executable 164 (which defaults to the Python interpreter) is linked against. 165 166 Returns a tuple of strings (lib,version) which default to the 167 given parameters in case the lookup fails. 168 169 Note that the function has intimate knowledge of how different 170 libc versions add symbols to the executable and thus is probably 171 only usable for executables compiled using gcc. 172 173 The file is read and scanned in chunks of chunksize bytes. 174 175 """ 176 if not executable: 177 try: 178 ver = os.confstr('CS_GNU_LIBC_VERSION') 179 # parse 'glibc 2.28' as ('glibc', '2.28') 180 parts = ver.split(maxsplit=1) 181 if len(parts) == 2: 182 return tuple(parts) 183 except (AttributeError, ValueError, OSError): 184 # os.confstr() or CS_GNU_LIBC_VERSION value not available 185 pass 186 187 executable = sys.executable 188 189 if not executable: 190 # sys.executable is not set. 191 return lib, version 192 193 V = _comparable_version 194 # We use os.path.realpath() 195 # here to work around problems with Cygwin not being 196 # able to open symlinks for reading 197 executable = os.path.realpath(executable) 198 with open(executable, 'rb') as f: 199 binary = f.read(chunksize) 200 pos = 0 201 while pos < len(binary): 202 if b'libc' in binary or b'GLIBC' in binary: 203 m = _libc_search.search(binary, pos) 204 else: 205 m = None 206 if not m or m.end() == len(binary): 207 chunk = f.read(chunksize) 208 if chunk: 209 binary = binary[max(pos, len(binary) - 1000):] + chunk 210 pos = 0 211 continue 212 if not m: 213 break 214 libcinit, glibc, glibcversion, so, threads, soversion = [ 215 s.decode('latin1') if s is not None else s 216 for s in m.groups()] 217 if libcinit and not lib: 218 lib = 'libc' 219 elif glibc: 220 if lib != 'glibc': 221 lib = 'glibc' 222 version = glibcversion 223 elif V(glibcversion) > V(version): 224 version = glibcversion 225 elif so: 226 if lib != 'glibc': 227 lib = 'libc' 228 if soversion and (not version or V(soversion) > V(version)): 229 version = soversion 230 if threads and version[-len(threads):] != threads: 231 version = version + threads 232 pos = m.end() 233 return lib, version 234 235def _norm_version(version, build=''): 236 237 """ Normalize the version and build strings and return a single 238 version string using the format major.minor.build (or patchlevel). 239 """ 240 l = version.split('.') 241 if build: 242 l.append(build) 243 try: 244 strings = list(map(str, map(int, l))) 245 except ValueError: 246 strings = l 247 version = '.'.join(strings[:3]) 248 return version 249 250_ver_output = re.compile(r'(?:([\w ]+) ([\w.]+) ' 251 r'.*' 252 r'\[.* ([\d.]+)\])') 253 254# Examples of VER command output: 255# 256# Windows 2000: Microsoft Windows 2000 [Version 5.00.2195] 257# Windows XP: Microsoft Windows XP [Version 5.1.2600] 258# Windows Vista: Microsoft Windows [Version 6.0.6002] 259# 260# Note that the "Version" string gets localized on different 261# Windows versions. 262 263def _syscmd_ver(system='', release='', version='', 264 265 supported_platforms=('win32', 'win16', 'dos')): 266 267 """ Tries to figure out the OS version used and returns 268 a tuple (system, release, version). 269 270 It uses the "ver" shell command for this which is known 271 to exists on Windows, DOS. XXX Others too ? 272 273 In case this fails, the given parameters are used as 274 defaults. 275 276 """ 277 if sys.platform not in supported_platforms: 278 return system, release, version 279 280 # Try some common cmd strings 281 import subprocess 282 for cmd in ('ver', 'command /c ver', 'cmd /c ver'): 283 try: 284 info = subprocess.check_output(cmd, 285 stdin=subprocess.DEVNULL, 286 stderr=subprocess.DEVNULL, 287 text=True, 288 encoding="locale", 289 shell=True) 290 except (OSError, subprocess.CalledProcessError) as why: 291 #print('Command %s failed: %s' % (cmd, why)) 292 continue 293 else: 294 break 295 else: 296 return system, release, version 297 298 # Parse the output 299 info = info.strip() 300 m = _ver_output.match(info) 301 if m is not None: 302 system, release, version = m.groups() 303 # Strip trailing dots from version and release 304 if release[-1] == '.': 305 release = release[:-1] 306 if version[-1] == '.': 307 version = version[:-1] 308 # Normalize the version and build strings (eliminating additional 309 # zeros) 310 version = _norm_version(version) 311 return system, release, version 312 313_WIN32_CLIENT_RELEASES = { 314 (5, 0): "2000", 315 (5, 1): "XP", 316 # Strictly, 5.2 client is XP 64-bit, but platform.py historically 317 # has always called it 2003 Server 318 (5, 2): "2003Server", 319 (5, None): "post2003", 320 321 (6, 0): "Vista", 322 (6, 1): "7", 323 (6, 2): "8", 324 (6, 3): "8.1", 325 (6, None): "post8.1", 326 327 (10, 0): "10", 328 (10, None): "post10", 329} 330 331# Server release name lookup will default to client names if necessary 332_WIN32_SERVER_RELEASES = { 333 (5, 2): "2003Server", 334 335 (6, 0): "2008Server", 336 (6, 1): "2008ServerR2", 337 (6, 2): "2012Server", 338 (6, 3): "2012ServerR2", 339 (6, None): "post2012ServerR2", 340} 341 342def win32_is_iot(): 343 return win32_edition() in ('IoTUAP', 'NanoServer', 'WindowsCoreHeadless', 'IoTEdgeOS') 344 345def win32_edition(): 346 try: 347 try: 348 import winreg 349 except ImportError: 350 import _winreg as winreg 351 except ImportError: 352 pass 353 else: 354 try: 355 cvkey = r'SOFTWARE\Microsoft\Windows NT\CurrentVersion' 356 with winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, cvkey) as key: 357 return winreg.QueryValueEx(key, 'EditionId')[0] 358 except OSError: 359 pass 360 361 return None 362 363def win32_ver(release='', version='', csd='', ptype=''): 364 try: 365 from sys import getwindowsversion 366 except ImportError: 367 return release, version, csd, ptype 368 369 winver = getwindowsversion() 370 try: 371 major, minor, build = map(int, _syscmd_ver()[2].split('.')) 372 except ValueError: 373 major, minor, build = winver.platform_version or winver[:3] 374 version = '{0}.{1}.{2}'.format(major, minor, build) 375 376 release = (_WIN32_CLIENT_RELEASES.get((major, minor)) or 377 _WIN32_CLIENT_RELEASES.get((major, None)) or 378 release) 379 380 # getwindowsversion() reflect the compatibility mode Python is 381 # running under, and so the service pack value is only going to be 382 # valid if the versions match. 383 if winver[:2] == (major, minor): 384 try: 385 csd = 'SP{}'.format(winver.service_pack_major) 386 except AttributeError: 387 if csd[:13] == 'Service Pack ': 388 csd = 'SP' + csd[13:] 389 390 # VER_NT_SERVER = 3 391 if getattr(winver, 'product_type', None) == 3: 392 release = (_WIN32_SERVER_RELEASES.get((major, minor)) or 393 _WIN32_SERVER_RELEASES.get((major, None)) or 394 release) 395 396 try: 397 try: 398 import winreg 399 except ImportError: 400 import _winreg as winreg 401 except ImportError: 402 pass 403 else: 404 try: 405 cvkey = r'SOFTWARE\Microsoft\Windows NT\CurrentVersion' 406 with winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, cvkey) as key: 407 ptype = winreg.QueryValueEx(key, 'CurrentType')[0] 408 except OSError: 409 pass 410 411 return release, version, csd, ptype 412 413 414def _mac_ver_xml(): 415 fn = '/System/Library/CoreServices/SystemVersion.plist' 416 if not os.path.exists(fn): 417 return None 418 419 try: 420 import plistlib 421 except ImportError: 422 return None 423 424 with open(fn, 'rb') as f: 425 pl = plistlib.load(f) 426 release = pl['ProductVersion'] 427 versioninfo = ('', '', '') 428 machine = os.uname().machine 429 if machine in ('ppc', 'Power Macintosh'): 430 # Canonical name 431 machine = 'PowerPC' 432 433 return release, versioninfo, machine 434 435 436def mac_ver(release='', versioninfo=('', '', ''), machine=''): 437 438 """ Get macOS version information and return it as tuple (release, 439 versioninfo, machine) with versioninfo being a tuple (version, 440 dev_stage, non_release_version). 441 442 Entries which cannot be determined are set to the parameter values 443 which default to ''. All tuple entries are strings. 444 """ 445 446 # First try reading the information from an XML file which should 447 # always be present 448 info = _mac_ver_xml() 449 if info is not None: 450 return info 451 452 # If that also doesn't work return the default values 453 return release, versioninfo, machine 454 455def _java_getprop(name, default): 456 457 from java.lang import System 458 try: 459 value = System.getProperty(name) 460 if value is None: 461 return default 462 return value 463 except AttributeError: 464 return default 465 466def java_ver(release='', vendor='', vminfo=('', '', ''), osinfo=('', '', '')): 467 468 """ Version interface for Jython. 469 470 Returns a tuple (release, vendor, vminfo, osinfo) with vminfo being 471 a tuple (vm_name, vm_release, vm_vendor) and osinfo being a 472 tuple (os_name, os_version, os_arch). 473 474 Values which cannot be determined are set to the defaults 475 given as parameters (which all default to ''). 476 477 """ 478 # Import the needed APIs 479 try: 480 import java.lang 481 except ImportError: 482 return release, vendor, vminfo, osinfo 483 484 vendor = _java_getprop('java.vendor', vendor) 485 release = _java_getprop('java.version', release) 486 vm_name, vm_release, vm_vendor = vminfo 487 vm_name = _java_getprop('java.vm.name', vm_name) 488 vm_vendor = _java_getprop('java.vm.vendor', vm_vendor) 489 vm_release = _java_getprop('java.vm.version', vm_release) 490 vminfo = vm_name, vm_release, vm_vendor 491 os_name, os_version, os_arch = osinfo 492 os_arch = _java_getprop('java.os.arch', os_arch) 493 os_name = _java_getprop('java.os.name', os_name) 494 os_version = _java_getprop('java.os.version', os_version) 495 osinfo = os_name, os_version, os_arch 496 497 return release, vendor, vminfo, osinfo 498 499### System name aliasing 500 501def system_alias(system, release, version): 502 503 """ Returns (system, release, version) aliased to common 504 marketing names used for some systems. 505 506 It also does some reordering of the information in some cases 507 where it would otherwise cause confusion. 508 509 """ 510 if system == 'SunOS': 511 # Sun's OS 512 if release < '5': 513 # These releases use the old name SunOS 514 return system, release, version 515 # Modify release (marketing release = SunOS release - 3) 516 l = release.split('.') 517 if l: 518 try: 519 major = int(l[0]) 520 except ValueError: 521 pass 522 else: 523 major = major - 3 524 l[0] = str(major) 525 release = '.'.join(l) 526 if release < '6': 527 system = 'Solaris' 528 else: 529 # XXX Whatever the new SunOS marketing name is... 530 system = 'Solaris' 531 532 elif system in ('win32', 'win16'): 533 # In case one of the other tricks 534 system = 'Windows' 535 536 # bpo-35516: Don't replace Darwin with macOS since input release and 537 # version arguments can be different than the currently running version. 538 539 return system, release, version 540 541### Various internal helpers 542 543def _platform(*args): 544 545 """ Helper to format the platform string in a filename 546 compatible format e.g. "system-version-machine". 547 """ 548 # Format the platform string 549 platform = '-'.join(x.strip() for x in filter(len, args)) 550 551 # Cleanup some possible filename obstacles... 552 platform = platform.replace(' ', '_') 553 platform = platform.replace('/', '-') 554 platform = platform.replace('\\', '-') 555 platform = platform.replace(':', '-') 556 platform = platform.replace(';', '-') 557 platform = platform.replace('"', '-') 558 platform = platform.replace('(', '-') 559 platform = platform.replace(')', '-') 560 561 # No need to report 'unknown' information... 562 platform = platform.replace('unknown', '') 563 564 # Fold '--'s and remove trailing '-' 565 while 1: 566 cleaned = platform.replace('--', '-') 567 if cleaned == platform: 568 break 569 platform = cleaned 570 while platform[-1] == '-': 571 platform = platform[:-1] 572 573 return platform 574 575def _node(default=''): 576 577 """ Helper to determine the node name of this machine. 578 """ 579 try: 580 import socket 581 except ImportError: 582 # No sockets... 583 return default 584 try: 585 return socket.gethostname() 586 except OSError: 587 # Still not working... 588 return default 589 590def _follow_symlinks(filepath): 591 592 """ In case filepath is a symlink, follow it until a 593 real file is reached. 594 """ 595 filepath = os.path.abspath(filepath) 596 while os.path.islink(filepath): 597 filepath = os.path.normpath( 598 os.path.join(os.path.dirname(filepath), os.readlink(filepath))) 599 return filepath 600 601 602def _syscmd_file(target, default=''): 603 604 """ Interface to the system's file command. 605 606 The function uses the -b option of the file command to have it 607 omit the filename in its output. Follow the symlinks. It returns 608 default in case the command should fail. 609 610 """ 611 if sys.platform in ('dos', 'win32', 'win16'): 612 # XXX Others too ? 613 return default 614 615 try: 616 import subprocess 617 except ImportError: 618 return default 619 target = _follow_symlinks(target) 620 # "file" output is locale dependent: force the usage of the C locale 621 # to get deterministic behavior. 622 env = dict(os.environ, LC_ALL='C') 623 try: 624 # -b: do not prepend filenames to output lines (brief mode) 625 output = subprocess.check_output(['file', '-b', target], 626 stderr=subprocess.DEVNULL, 627 env=env) 628 except (OSError, subprocess.CalledProcessError): 629 return default 630 if not output: 631 return default 632 # With the C locale, the output should be mostly ASCII-compatible. 633 # Decode from Latin-1 to prevent Unicode decode error. 634 return output.decode('latin-1') 635 636### Information about the used architecture 637 638# Default values for architecture; non-empty strings override the 639# defaults given as parameters 640_default_architecture = { 641 'win32': ('', 'WindowsPE'), 642 'win16': ('', 'Windows'), 643 'dos': ('', 'MSDOS'), 644} 645 646def architecture(executable=sys.executable, bits='', linkage=''): 647 648 """ Queries the given executable (defaults to the Python interpreter 649 binary) for various architecture information. 650 651 Returns a tuple (bits, linkage) which contains information about 652 the bit architecture and the linkage format used for the 653 executable. Both values are returned as strings. 654 655 Values that cannot be determined are returned as given by the 656 parameter presets. If bits is given as '', the sizeof(pointer) 657 (or sizeof(long) on Python version < 1.5.2) is used as 658 indicator for the supported pointer size. 659 660 The function relies on the system's "file" command to do the 661 actual work. This is available on most if not all Unix 662 platforms. On some non-Unix platforms where the "file" command 663 does not exist and the executable is set to the Python interpreter 664 binary defaults from _default_architecture are used. 665 666 """ 667 # Use the sizeof(pointer) as default number of bits if nothing 668 # else is given as default. 669 if not bits: 670 import struct 671 size = struct.calcsize('P') 672 bits = str(size * 8) + 'bit' 673 674 # Get data from the 'file' system command 675 if executable: 676 fileout = _syscmd_file(executable, '') 677 else: 678 fileout = '' 679 680 if not fileout and \ 681 executable == sys.executable: 682 # "file" command did not return anything; we'll try to provide 683 # some sensible defaults then... 684 if sys.platform in _default_architecture: 685 b, l = _default_architecture[sys.platform] 686 if b: 687 bits = b 688 if l: 689 linkage = l 690 return bits, linkage 691 692 if 'executable' not in fileout and 'shared object' not in fileout: 693 # Format not supported 694 return bits, linkage 695 696 # Bits 697 if '32-bit' in fileout: 698 bits = '32bit' 699 elif '64-bit' in fileout: 700 bits = '64bit' 701 702 # Linkage 703 if 'ELF' in fileout: 704 linkage = 'ELF' 705 elif 'PE' in fileout: 706 # E.g. Windows uses this format 707 if 'Windows' in fileout: 708 linkage = 'WindowsPE' 709 else: 710 linkage = 'PE' 711 elif 'COFF' in fileout: 712 linkage = 'COFF' 713 elif 'MS-DOS' in fileout: 714 linkage = 'MSDOS' 715 else: 716 # XXX the A.OUT format also falls under this class... 717 pass 718 719 return bits, linkage 720 721 722def _get_machine_win32(): 723 # Try to use the PROCESSOR_* environment variables 724 # available on Win XP and later; see 725 # http://support.microsoft.com/kb/888731 and 726 # http://www.geocities.com/rick_lively/MANUALS/ENV/MSWIN/PROCESSI.HTM 727 728 # WOW64 processes mask the native architecture 729 return ( 730 os.environ.get('PROCESSOR_ARCHITEW6432', '') or 731 os.environ.get('PROCESSOR_ARCHITECTURE', '') 732 ) 733 734 735class _Processor: 736 @classmethod 737 def get(cls): 738 func = getattr(cls, f'get_{sys.platform}', cls.from_subprocess) 739 return func() or '' 740 741 def get_win32(): 742 return os.environ.get('PROCESSOR_IDENTIFIER', _get_machine_win32()) 743 744 def get_OpenVMS(): 745 try: 746 import vms_lib 747 except ImportError: 748 pass 749 else: 750 csid, cpu_number = vms_lib.getsyi('SYI$_CPU', 0) 751 return 'Alpha' if cpu_number >= 128 else 'VAX' 752 753 def from_subprocess(): 754 """ 755 Fall back to `uname -p` 756 """ 757 try: 758 import subprocess 759 except ImportError: 760 return None 761 try: 762 return subprocess.check_output( 763 ['uname', '-p'], 764 stderr=subprocess.DEVNULL, 765 text=True, 766 encoding="utf8", 767 ).strip() 768 except (OSError, subprocess.CalledProcessError): 769 pass 770 771 772def _unknown_as_blank(val): 773 return '' if val == 'unknown' else val 774 775 776### Portable uname() interface 777 778class uname_result( 779 collections.namedtuple( 780 "uname_result_base", 781 "system node release version machine") 782 ): 783 """ 784 A uname_result that's largely compatible with a 785 simple namedtuple except that 'processor' is 786 resolved late and cached to avoid calling "uname" 787 except when needed. 788 """ 789 790 _fields = ('system', 'node', 'release', 'version', 'machine', 'processor') 791 792 @functools.cached_property 793 def processor(self): 794 return _unknown_as_blank(_Processor.get()) 795 796 def __iter__(self): 797 return itertools.chain( 798 super().__iter__(), 799 (self.processor,) 800 ) 801 802 @classmethod 803 def _make(cls, iterable): 804 # override factory to affect length check 805 num_fields = len(cls._fields) - 1 806 result = cls.__new__(cls, *iterable) 807 if len(result) != num_fields + 1: 808 msg = f'Expected {num_fields} arguments, got {len(result)}' 809 raise TypeError(msg) 810 return result 811 812 def __getitem__(self, key): 813 return tuple(self)[key] 814 815 def __len__(self): 816 return len(tuple(iter(self))) 817 818 def __reduce__(self): 819 return uname_result, tuple(self)[:len(self._fields) - 1] 820 821 822_uname_cache = None 823 824 825def uname(): 826 827 """ Fairly portable uname interface. Returns a tuple 828 of strings (system, node, release, version, machine, processor) 829 identifying the underlying platform. 830 831 Note that unlike the os.uname function this also returns 832 possible processor information as an additional tuple entry. 833 834 Entries which cannot be determined are set to ''. 835 836 """ 837 global _uname_cache 838 839 if _uname_cache is not None: 840 return _uname_cache 841 842 # Get some infos from the builtin os.uname API... 843 try: 844 system, node, release, version, machine = infos = os.uname() 845 except AttributeError: 846 system = sys.platform 847 node = _node() 848 release = version = machine = '' 849 infos = () 850 851 if not any(infos): 852 # uname is not available 853 854 # Try win32_ver() on win32 platforms 855 if system == 'win32': 856 release, version, csd, ptype = win32_ver() 857 machine = machine or _get_machine_win32() 858 859 # Try the 'ver' system command available on some 860 # platforms 861 if not (release and version): 862 system, release, version = _syscmd_ver(system) 863 # Normalize system to what win32_ver() normally returns 864 # (_syscmd_ver() tends to return the vendor name as well) 865 if system == 'Microsoft Windows': 866 system = 'Windows' 867 elif system == 'Microsoft' and release == 'Windows': 868 # Under Windows Vista and Windows Server 2008, 869 # Microsoft changed the output of the ver command. The 870 # release is no longer printed. This causes the 871 # system and release to be misidentified. 872 system = 'Windows' 873 if '6.0' == version[:3]: 874 release = 'Vista' 875 else: 876 release = '' 877 878 # In case we still don't know anything useful, we'll try to 879 # help ourselves 880 if system in ('win32', 'win16'): 881 if not version: 882 if system == 'win32': 883 version = '32bit' 884 else: 885 version = '16bit' 886 system = 'Windows' 887 888 elif system[:4] == 'java': 889 release, vendor, vminfo, osinfo = java_ver() 890 system = 'Java' 891 version = ', '.join(vminfo) 892 if not version: 893 version = vendor 894 895 # System specific extensions 896 if system == 'OpenVMS': 897 # OpenVMS seems to have release and version mixed up 898 if not release or release == '0': 899 release = version 900 version = '' 901 902 # normalize name 903 if system == 'Microsoft' and release == 'Windows': 904 system = 'Windows' 905 release = 'Vista' 906 907 vals = system, node, release, version, machine 908 # Replace 'unknown' values with the more portable '' 909 _uname_cache = uname_result(*map(_unknown_as_blank, vals)) 910 return _uname_cache 911 912### Direct interfaces to some of the uname() return values 913 914def system(): 915 916 """ Returns the system/OS name, e.g. 'Linux', 'Windows' or 'Java'. 917 918 An empty string is returned if the value cannot be determined. 919 920 """ 921 return uname().system 922 923def node(): 924 925 """ Returns the computer's network name (which may not be fully 926 qualified) 927 928 An empty string is returned if the value cannot be determined. 929 930 """ 931 return uname().node 932 933def release(): 934 935 """ Returns the system's release, e.g. '2.2.0' or 'NT' 936 937 An empty string is returned if the value cannot be determined. 938 939 """ 940 return uname().release 941 942def version(): 943 944 """ Returns the system's release version, e.g. '#3 on degas' 945 946 An empty string is returned if the value cannot be determined. 947 948 """ 949 return uname().version 950 951def machine(): 952 953 """ Returns the machine type, e.g. 'i386' 954 955 An empty string is returned if the value cannot be determined. 956 957 """ 958 return uname().machine 959 960def processor(): 961 962 """ Returns the (true) processor name, e.g. 'amdk6' 963 964 An empty string is returned if the value cannot be 965 determined. Note that many platforms do not provide this 966 information or simply return the same value as for machine(), 967 e.g. NetBSD does this. 968 969 """ 970 return uname().processor 971 972### Various APIs for extracting information from sys.version 973 974_sys_version_parser = re.compile( 975 r'([\w.+]+)\s*' # "version<space>" 976 r'\(#?([^,]+)' # "(#buildno" 977 r'(?:,\s*([\w ]*)' # ", builddate" 978 r'(?:,\s*([\w :]*))?)?\)\s*' # ", buildtime)<space>" 979 r'\[([^\]]+)\]?', re.ASCII) # "[compiler]" 980 981_ironpython_sys_version_parser = re.compile( 982 r'IronPython\s*' 983 r'([\d\.]+)' 984 r'(?: \(([\d\.]+)\))?' 985 r' on (.NET [\d\.]+)', re.ASCII) 986 987# IronPython covering 2.6 and 2.7 988_ironpython26_sys_version_parser = re.compile( 989 r'([\d.]+)\s*' 990 r'\(IronPython\s*' 991 r'[\d.]+\s*' 992 r'\(([\d.]+)\) on ([\w.]+ [\d.]+(?: \(\d+-bit\))?)\)' 993) 994 995_pypy_sys_version_parser = re.compile( 996 r'([\w.+]+)\s*' 997 r'\(#?([^,]+),\s*([\w ]+),\s*([\w :]+)\)\s*' 998 r'\[PyPy [^\]]+\]?') 999 1000_sys_version_cache = {} 1001 1002def _sys_version(sys_version=None): 1003 1004 """ Returns a parsed version of Python's sys.version as tuple 1005 (name, version, branch, revision, buildno, builddate, compiler) 1006 referring to the Python implementation name, version, branch, 1007 revision, build number, build date/time as string and the compiler 1008 identification string. 1009 1010 Note that unlike the Python sys.version, the returned value 1011 for the Python version will always include the patchlevel (it 1012 defaults to '.0'). 1013 1014 The function returns empty strings for tuple entries that 1015 cannot be determined. 1016 1017 sys_version may be given to parse an alternative version 1018 string, e.g. if the version was read from a different Python 1019 interpreter. 1020 1021 """ 1022 # Get the Python version 1023 if sys_version is None: 1024 sys_version = sys.version 1025 1026 # Try the cache first 1027 result = _sys_version_cache.get(sys_version, None) 1028 if result is not None: 1029 return result 1030 1031 # Parse it 1032 if 'IronPython' in sys_version: 1033 # IronPython 1034 name = 'IronPython' 1035 if sys_version.startswith('IronPython'): 1036 match = _ironpython_sys_version_parser.match(sys_version) 1037 else: 1038 match = _ironpython26_sys_version_parser.match(sys_version) 1039 1040 if match is None: 1041 raise ValueError( 1042 'failed to parse IronPython sys.version: %s' % 1043 repr(sys_version)) 1044 1045 version, alt_version, compiler = match.groups() 1046 buildno = '' 1047 builddate = '' 1048 1049 elif sys.platform.startswith('java'): 1050 # Jython 1051 name = 'Jython' 1052 match = _sys_version_parser.match(sys_version) 1053 if match is None: 1054 raise ValueError( 1055 'failed to parse Jython sys.version: %s' % 1056 repr(sys_version)) 1057 version, buildno, builddate, buildtime, _ = match.groups() 1058 if builddate is None: 1059 builddate = '' 1060 compiler = sys.platform 1061 1062 elif "PyPy" in sys_version: 1063 # PyPy 1064 name = "PyPy" 1065 match = _pypy_sys_version_parser.match(sys_version) 1066 if match is None: 1067 raise ValueError("failed to parse PyPy sys.version: %s" % 1068 repr(sys_version)) 1069 version, buildno, builddate, buildtime = match.groups() 1070 compiler = "" 1071 1072 else: 1073 # CPython 1074 match = _sys_version_parser.match(sys_version) 1075 if match is None: 1076 raise ValueError( 1077 'failed to parse CPython sys.version: %s' % 1078 repr(sys_version)) 1079 version, buildno, builddate, buildtime, compiler = \ 1080 match.groups() 1081 name = 'CPython' 1082 if builddate is None: 1083 builddate = '' 1084 elif buildtime: 1085 builddate = builddate + ' ' + buildtime 1086 1087 if hasattr(sys, '_git'): 1088 _, branch, revision = sys._git 1089 elif hasattr(sys, '_mercurial'): 1090 _, branch, revision = sys._mercurial 1091 else: 1092 branch = '' 1093 revision = '' 1094 1095 # Add the patchlevel version if missing 1096 l = version.split('.') 1097 if len(l) == 2: 1098 l.append('0') 1099 version = '.'.join(l) 1100 1101 # Build and cache the result 1102 result = (name, version, branch, revision, buildno, builddate, compiler) 1103 _sys_version_cache[sys_version] = result 1104 return result 1105 1106def python_implementation(): 1107 1108 """ Returns a string identifying the Python implementation. 1109 1110 Currently, the following implementations are identified: 1111 'CPython' (C implementation of Python), 1112 'IronPython' (.NET implementation of Python), 1113 'Jython' (Java implementation of Python), 1114 'PyPy' (Python implementation of Python). 1115 1116 """ 1117 return _sys_version()[0] 1118 1119def python_version(): 1120 1121 """ Returns the Python version as string 'major.minor.patchlevel' 1122 1123 Note that unlike the Python sys.version, the returned value 1124 will always include the patchlevel (it defaults to 0). 1125 1126 """ 1127 return _sys_version()[1] 1128 1129def python_version_tuple(): 1130 1131 """ Returns the Python version as tuple (major, minor, patchlevel) 1132 of strings. 1133 1134 Note that unlike the Python sys.version, the returned value 1135 will always include the patchlevel (it defaults to 0). 1136 1137 """ 1138 return tuple(_sys_version()[1].split('.')) 1139 1140def python_branch(): 1141 1142 """ Returns a string identifying the Python implementation 1143 branch. 1144 1145 For CPython this is the SCM branch from which the 1146 Python binary was built. 1147 1148 If not available, an empty string is returned. 1149 1150 """ 1151 1152 return _sys_version()[2] 1153 1154def python_revision(): 1155 1156 """ Returns a string identifying the Python implementation 1157 revision. 1158 1159 For CPython this is the SCM revision from which the 1160 Python binary was built. 1161 1162 If not available, an empty string is returned. 1163 1164 """ 1165 return _sys_version()[3] 1166 1167def python_build(): 1168 1169 """ Returns a tuple (buildno, builddate) stating the Python 1170 build number and date as strings. 1171 1172 """ 1173 return _sys_version()[4:6] 1174 1175def python_compiler(): 1176 1177 """ Returns a string identifying the compiler used for compiling 1178 Python. 1179 1180 """ 1181 return _sys_version()[6] 1182 1183### The Opus Magnum of platform strings :-) 1184 1185_platform_cache = {} 1186 1187def platform(aliased=0, terse=0): 1188 1189 """ Returns a single string identifying the underlying platform 1190 with as much useful information as possible (but no more :). 1191 1192 The output is intended to be human readable rather than 1193 machine parseable. It may look different on different 1194 platforms and this is intended. 1195 1196 If "aliased" is true, the function will use aliases for 1197 various platforms that report system names which differ from 1198 their common names, e.g. SunOS will be reported as 1199 Solaris. The system_alias() function is used to implement 1200 this. 1201 1202 Setting terse to true causes the function to return only the 1203 absolute minimum information needed to identify the platform. 1204 1205 """ 1206 result = _platform_cache.get((aliased, terse), None) 1207 if result is not None: 1208 return result 1209 1210 # Get uname information and then apply platform specific cosmetics 1211 # to it... 1212 system, node, release, version, machine, processor = uname() 1213 if machine == processor: 1214 processor = '' 1215 if aliased: 1216 system, release, version = system_alias(system, release, version) 1217 1218 if system == 'Darwin': 1219 # macOS (darwin kernel) 1220 macos_release = mac_ver()[0] 1221 if macos_release: 1222 system = 'macOS' 1223 release = macos_release 1224 1225 if system == 'Windows': 1226 # MS platforms 1227 rel, vers, csd, ptype = win32_ver(version) 1228 if terse: 1229 platform = _platform(system, release) 1230 else: 1231 platform = _platform(system, release, version, csd) 1232 1233 elif system in ('Linux',): 1234 # check for libc vs. glibc 1235 libcname, libcversion = libc_ver() 1236 platform = _platform(system, release, machine, processor, 1237 'with', 1238 libcname+libcversion) 1239 elif system == 'Java': 1240 # Java platforms 1241 r, v, vminfo, (os_name, os_version, os_arch) = java_ver() 1242 if terse or not os_name: 1243 platform = _platform(system, release, version) 1244 else: 1245 platform = _platform(system, release, version, 1246 'on', 1247 os_name, os_version, os_arch) 1248 1249 else: 1250 # Generic handler 1251 if terse: 1252 platform = _platform(system, release) 1253 else: 1254 bits, linkage = architecture(sys.executable) 1255 platform = _platform(system, release, machine, 1256 processor, bits, linkage) 1257 1258 _platform_cache[(aliased, terse)] = platform 1259 return platform 1260 1261### freedesktop.org os-release standard 1262# https://www.freedesktop.org/software/systemd/man/os-release.html 1263 1264# NAME=value with optional quotes (' or "). The regular expression is less 1265# strict than shell lexer, but that's ok. 1266_os_release_line = re.compile( 1267 "^(?P<name>[a-zA-Z0-9_]+)=(?P<quote>[\"\']?)(?P<value>.*)(?P=quote)$" 1268) 1269# unescape five special characters mentioned in the standard 1270_os_release_unescape = re.compile(r"\\([\\\$\"\'`])") 1271# /etc takes precedence over /usr/lib 1272_os_release_candidates = ("/etc/os-release", "/usr/lib/os-release") 1273_os_release_cache = None 1274 1275 1276def _parse_os_release(lines): 1277 # These fields are mandatory fields with well-known defaults 1278 # in practice all Linux distributions override NAME, ID, and PRETTY_NAME. 1279 info = { 1280 "NAME": "Linux", 1281 "ID": "linux", 1282 "PRETTY_NAME": "Linux", 1283 } 1284 1285 for line in lines: 1286 mo = _os_release_line.match(line) 1287 if mo is not None: 1288 info[mo.group('name')] = _os_release_unescape.sub( 1289 r"\1", mo.group('value') 1290 ) 1291 1292 return info 1293 1294 1295def freedesktop_os_release(): 1296 """Return operation system identification from freedesktop.org os-release 1297 """ 1298 global _os_release_cache 1299 1300 if _os_release_cache is None: 1301 errno = None 1302 for candidate in _os_release_candidates: 1303 try: 1304 with open(candidate, encoding="utf-8") as f: 1305 _os_release_cache = _parse_os_release(f) 1306 break 1307 except OSError as e: 1308 errno = e.errno 1309 else: 1310 raise OSError( 1311 errno, 1312 f"Unable to read files {', '.join(_os_release_candidates)}" 1313 ) 1314 1315 return _os_release_cache.copy() 1316 1317 1318### Command line interface 1319 1320if __name__ == '__main__': 1321 # Default is to print the aliased verbose platform string 1322 terse = ('terse' in sys.argv or '--terse' in sys.argv) 1323 aliased = (not 'nonaliased' in sys.argv and not '--nonaliased' in sys.argv) 1324 print(platform(aliased, terse)) 1325 sys.exit(0) 1326