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