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