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