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