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