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