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