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