qutebrowser/qutebrowser/utils/version.py

537 lines
16 KiB
Python
Raw Normal View History

2014-06-19 09:04:37 +02:00
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2018-02-05 12:19:50 +01:00
# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
2014-02-10 15:01:05 +01:00
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
2018-02-03 19:08:42 +01:00
"""Utilities to show various version information."""
2014-02-10 15:01:05 +01:00
2016-01-05 18:41:53 +01:00
import re
2014-02-10 15:01:05 +01:00
import sys
2014-02-20 15:32:46 +01:00
import glob
2014-02-10 15:01:05 +01:00
import os.path
import platform
import subprocess
import importlib
import collections
import enum
2017-11-14 03:17:44 +01:00
import datetime
2018-02-07 21:03:46 +01:00
import getpass
2014-02-10 15:01:05 +01:00
2017-09-19 22:18:02 +02:00
import attr
2017-11-14 09:05:28 +01:00
import pkg_resources
2017-06-19 09:42:49 +02:00
from PyQt5.QtCore import PYQT_VERSION_STR, QLibraryInfo
2015-06-07 10:46:47 +02:00
from PyQt5.QtNetwork import QSslSocket
from PyQt5.QtGui import (QOpenGLContext, QOpenGLVersionProfile,
QOffscreenSurface)
2015-06-24 20:37:48 +02:00
from PyQt5.QtWidgets import QApplication
2014-02-10 15:01:05 +01:00
try:
from PyQt5.QtWebKit import qWebKitVersion
except ImportError: # pragma: no cover
qWebKitVersion = None
try:
from PyQt5.QtWebEngineWidgets import QWebEngineProfile
except ImportError: # pragma: no cover
QWebEngineProfile = None
2014-02-10 15:01:05 +01:00
import qutebrowser
from qutebrowser.utils import log, utils, standarddir, usertypes, message
from qutebrowser.misc import objects, earlyinit, sql, httpclient, pastebin
2016-01-05 18:41:53 +01:00
from qutebrowser.browser import pdfjs
2014-02-10 15:01:05 +01:00
2017-09-19 22:18:02 +02:00
@attr.s
class DistributionInfo:
"""Information about the running distribution."""
id = attr.ib()
parsed = attr.ib()
version = attr.ib()
pretty = attr.ib()
2017-05-20 23:07:42 +02:00
2018-02-10 16:14:07 +01:00
pastebin_url = None
Distribution = enum.Enum(
2017-05-20 23:07:42 +02:00
'Distribution', ['unknown', 'ubuntu', 'debian', 'void', 'arch',
'gentoo', 'fedora', 'opensuse', 'linuxmint', 'manjaro'])
2017-05-30 07:37:10 +02:00
def distribution():
2017-05-20 23:07:42 +02:00
"""Get some information about the running Linux distribution.
Returns:
A DistributionInfo object, or None if no info could be determined.
parsed: A Distribution enum member
version: A Version object, or None
pretty: Always a string (might be "Unknown")
"""
2017-05-30 07:37:10 +02:00
filename = os.environ.get('QUTE_FAKE_OS_RELEASE', '/etc/os-release')
2017-05-20 23:07:42 +02:00
info = {}
try:
with open(filename, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if (not line) or line.startswith('#'):
continue
k, v = line.split("=", maxsplit=1)
info[k] = v.strip('"')
except (OSError, UnicodeDecodeError):
return None
pretty = info.get('PRETTY_NAME', 'Unknown')
2017-08-08 20:19:33 +02:00
if pretty == 'Linux': # Thanks, Funtoo
pretty = info.get('NAME', pretty)
2017-05-20 23:07:42 +02:00
if 'VERSION_ID' in info:
dist_version = pkg_resources.parse_version(info['VERSION_ID'])
else:
dist_version = None
dist_id = info.get('ID', None)
2017-08-08 20:19:33 +02:00
id_mappings = {
'funtoo': 'gentoo', # does not have ID_LIKE=gentoo
}
2017-05-20 23:07:42 +02:00
try:
2017-08-08 20:19:33 +02:00
parsed = Distribution[id_mappings.get(dist_id, dist_id)]
2017-05-20 23:07:42 +02:00
except KeyError:
parsed = Distribution.unknown
return DistributionInfo(parsed=parsed, version=dist_version, pretty=pretty,
id=dist_id)
def _git_str():
2014-02-19 10:58:32 +01:00
"""Try to find out git version.
2014-02-19 10:58:32 +01:00
Return:
string containing the git commit ID.
None if there was an error or we're not in a git repo.
"""
# First try via subprocess if possible
commit = None
if not hasattr(sys, "frozen"):
try:
gitpath = os.path.join(os.path.dirname(os.path.realpath(__file__)),
os.path.pardir, os.path.pardir)
except (NameError, OSError):
log.misc.exception("Error while getting git path")
else:
commit = _git_str_subprocess(gitpath)
if commit is not None:
return commit
# If that fails, check the git-commit-id file.
try:
2014-08-26 19:10:14 +02:00
return utils.read_file('git-commit-id')
except (OSError, ImportError):
return None
def _git_str_subprocess(gitpath):
2014-06-23 16:19:43 +02:00
"""Try to get the git commit ID and timestamp by calling git.
Args:
gitpath: The path where the .git folder is.
Return:
2014-06-23 16:19:43 +02:00
The ID/timestamp on success, None on failure.
"""
if not os.path.isdir(os.path.join(gitpath, ".git")):
return None
try:
# https://stackoverflow.com/questions/21017300/21017394#21017394
commit_hash = subprocess.run(
['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'],
cwd=gitpath, check=True,
stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
date = subprocess.run(
2014-06-23 16:19:43 +02:00
['git', 'show', '-s', '--format=%ci', 'HEAD'],
cwd=gitpath, check=True,
stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
return '{} ({})'.format(commit_hash, date)
2014-11-23 17:56:14 +01:00
except (subprocess.CalledProcessError, OSError):
return None
2014-02-20 15:32:46 +01:00
def _release_info():
2018-02-03 19:08:42 +01:00
"""Try to gather distribution release information.
2014-02-20 15:32:46 +01:00
Return:
list of (filename, content) tuples.
"""
2016-07-06 23:47:59 +02:00
blacklisted = ['ANSI_COLOR=', 'HOME_URL=', 'SUPPORT_URL=',
'BUG_REPORT_URL=']
2014-02-20 15:32:46 +01:00
data = []
for fn in glob.glob("/etc/*-release"):
2016-07-06 23:47:59 +02:00
lines = []
2014-02-20 15:32:46 +01:00
try:
2014-08-20 20:33:14 +02:00
with open(fn, 'r', encoding='utf-8') as f:
2016-07-06 23:47:59 +02:00
for line in f.read().strip().splitlines():
if not any(line.startswith(bl) for bl in blacklisted):
lines.append(line)
if lines:
data.append((fn, '\n'.join(lines)))
except OSError:
log.misc.exception("Error while reading {}.".format(fn))
2014-02-20 15:32:46 +01:00
return data
2014-02-20 19:56:34 +01:00
2014-06-06 18:54:47 +02:00
def _module_versions():
"""Get versions of optional modules.
2014-02-20 15:32:46 +01:00
2014-06-06 18:54:47 +02:00
Return:
A list of lines with version info.
"""
lines = []
modules = collections.OrderedDict([
('sip', ['SIP_VERSION_STR']),
('colorama', ['VERSION', '__version__']),
('pypeg2', ['__version__']),
('jinja2', ['__version__']),
('pygments', ['__version__']),
('yaml', ['__version__']),
('cssutils', ['__version__']),
2017-09-19 22:18:02 +02:00
('attr', ['__version__']),
2016-07-06 23:47:59 +02:00
('PyQt5.QtWebEngineWidgets', []),
('PyQt5.QtWebKitWidgets', []),
])
2017-09-19 22:18:02 +02:00
for modname, attributes in modules.items():
try:
2017-09-19 22:18:02 +02:00
module = importlib.import_module(modname)
except ImportError:
2017-09-19 22:18:02 +02:00
text = '{}: no'.format(modname)
else:
2017-09-19 22:18:02 +02:00
for name in attributes:
try:
2017-09-19 22:18:02 +02:00
text = '{}: {}'.format(modname, getattr(module, name))
except AttributeError:
pass
else:
break
else:
2017-09-19 22:18:02 +02:00
text = '{}: yes'.format(modname)
lines.append(text)
2014-06-06 18:54:47 +02:00
return lines
def _path_info():
"""Get info about important path names.
Return:
A dictionary of descriptive to actual path names.
"""
info = {
'config': standarddir.config(),
'data': standarddir.data(),
'cache': standarddir.cache(),
'runtime': standarddir.runtime(),
}
if standarddir.config() != standarddir.config(auto=True):
info['auto config'] = standarddir.config(auto=True)
if standarddir.data() != standarddir.data(system=True):
info['system data'] = standarddir.data(system=True)
return info
2014-06-06 18:54:47 +02:00
def _os_info():
"""Get operating system info.
Return:
A list of lines with version info.
"""
lines = []
releaseinfo = None
if utils.is_linux:
osver = ''
2014-06-06 18:54:47 +02:00
releaseinfo = _release_info()
elif utils.is_windows:
2014-06-06 18:54:47 +02:00
osver = ', '.join(platform.win32_ver())
elif utils.is_mac:
2014-09-25 22:16:37 +02:00
release, versioninfo, machine = platform.mac_ver()
if all(not e for e in versioninfo):
versioninfo = ''
else:
versioninfo = '.'.join(versioninfo)
osver = ', '.join([e for e in [release, versioninfo, machine] if e])
elif utils.is_posix:
osver = ' '.join(platform.uname())
2014-06-06 18:54:47 +02:00
else:
osver = '?'
lines.append('OS Version: {}'.format(osver))
if releaseinfo is not None:
for (fn, data) in releaseinfo:
lines += ['', '--- {} ---'.format(fn), data]
return lines
2014-02-20 15:32:46 +01:00
2014-06-06 18:54:47 +02:00
2016-01-05 18:41:53 +01:00
def _pdfjs_version():
"""Get the pdf.js version.
Return:
A string with the version number.
"""
try:
pdfjs_file, file_path = pdfjs.get_pdfjs_res_and_path('build/pdf.js')
2016-01-05 18:41:53 +01:00
except pdfjs.PDFJSNotFound:
return 'no'
else:
pdfjs_file = pdfjs_file.decode('utf-8')
version_re = re.compile(
2017-02-01 09:51:50 +01:00
r"^ *(PDFJS\.version|var pdfjsVersion) = '([^']+)';$",
re.MULTILINE)
2016-01-05 18:41:53 +01:00
match = version_re.search(pdfjs_file)
if not match:
pdfjs_version = 'unknown'
2016-01-05 18:41:53 +01:00
else:
pdfjs_version = match.group(2)
if file_path is None:
file_path = 'bundled'
return '{} ({})'.format(pdfjs_version, file_path)
2016-01-05 18:41:53 +01:00
def _chromium_version():
"""Get the Chromium version for QtWebEngine.
This can also be checked by looking at this file with the right Qt tag:
http://code.qt.io/cgit/qt/qtwebengine.git/tree/tools/scripts/version_resolver.py#n41
Quick reference:
Qt 5.7: Chromium 49
2018-09-27 13:41:25 +02:00
49.0.2623.111 (2016-03-31)
5.7.1: Security fixes up to 54.0.2840.87 (2016-11-01)
Qt 5.8: Chromium 53
53.0.2785.148 (2016-08-31)
5.8.0: Security fixes up to 55.0.2883.75 (2016-12-01)
Qt 5.9: Chromium 56
(LTS) 56.0.2924.122 (2017-01-25)
2018-09-27 13:41:25 +02:00
5.9.6: Security fixes up to 66.0.3359.170 (2018-05-10)
Qt 5.10: Chromium 61
61.0.3163.140 (2017-09-05)
2018-09-27 13:41:25 +02:00
5.10.1: Security fixes up to 64.0.3282.140 (2018-02-01)
Qt 5.11: Chromium 65
65.0.3325.151 (.1: .230) (2018-03-06)
2018-09-27 13:41:25 +02:00
5.11.2: Security fixes up to 68.0.3440.75 (2018-07-24)
2018-09-26 06:32:09 +02:00
Qt 5.12: Chromium 69
current 5.12 branch: 69.0.3497.70 (2018-09-11)
Also see https://www.chromium.org/developers/calendar
2018-09-27 13:41:25 +02:00
and https://chromereleases.googleblog.com/
"""
if QWebEngineProfile is None:
# This should never happen
return 'unavailable'
profile = QWebEngineProfile()
ua = profile.httpUserAgent()
match = re.search(r' Chrome/([^ ]*) ', ua)
if not match:
log.misc.error("Could not get Chromium version from: {}".format(ua))
return 'unknown'
return match.group(1)
def _backend():
"""Get the backend line with relevant information."""
if objects.backend == usertypes.Backend.QtWebKit:
2017-09-18 21:15:14 +02:00
return 'new QtWebKit (WebKit {})'.format(qWebKitVersion())
else:
2017-03-02 21:10:31 +01:00
webengine = usertypes.Backend.QtWebEngine
assert objects.backend == webengine, objects.backend
return 'QtWebEngine (Chromium {})'.format(_chromium_version())
2017-11-14 09:05:28 +01:00
2017-11-14 03:17:44 +01:00
def _uptime() -> datetime.timedelta:
launch_time = QApplication.instance().launch_time
time_delta = datetime.datetime.now() - launch_time
# Round off microseconds
time_delta -= datetime.timedelta(microseconds=time_delta.microseconds)
return time_delta
2017-11-14 09:05:28 +01:00
2016-07-06 23:47:59 +02:00
def version():
2018-02-03 19:08:42 +01:00
"""Return a string with various version information."""
2014-06-06 18:54:47 +02:00
lines = ["qutebrowser v{}".format(qutebrowser.__version__)]
gitver = _git_str()
if gitver is not None:
lines.append("Git commit: {}".format(gitver))
2017-02-08 18:34:50 +01:00
lines.append("Backend: {}".format(_backend()))
2016-07-06 23:47:59 +02:00
2014-06-06 18:54:47 +02:00
lines += [
'',
2014-06-10 22:59:14 +02:00
'{}: {}'.format(platform.python_implementation(),
2014-06-12 15:17:27 +02:00
platform.python_version()),
'Qt: {}'.format(earlyinit.qt_version()),
2014-06-10 22:59:14 +02:00
'PyQt: {}'.format(PYQT_VERSION_STR),
2016-07-06 23:47:59 +02:00
'',
2014-06-06 18:54:47 +02:00
]
2015-06-07 10:46:47 +02:00
2016-07-06 23:47:59 +02:00
lines += _module_versions()
lines += [
'pdf.js: {}'.format(_pdfjs_version()),
'sqlite: {}'.format(sql.version()),
'QtNetwork SSL: {}\n'.format(QSslSocket.sslLibraryVersionString()
if QSslSocket.supportsSsl() else 'no'),
2016-07-06 23:47:59 +02:00
]
qapp = QApplication.instance()
if qapp:
style = qapp.style()
lines.append('Style: {}'.format(style.metaObject().className()))
importpath = os.path.dirname(os.path.abspath(qutebrowser.__file__))
lines += [
'Platform: {}, {}'.format(platform.platform(),
platform.architecture()[0]),
]
dist = distribution()
if dist is not None:
lines += [
'Linux distribution: {} ({})'.format(dist.pretty, dist.parsed.name)
]
lines += [
2016-07-06 23:47:59 +02:00
'Frozen: {}'.format(hasattr(sys, 'frozen')),
"Imported from {}".format(importpath),
2018-05-14 22:13:15 +02:00
"Using Python from {}".format(sys.executable),
"Qt library executable path: {}, data path: {}".format(
QLibraryInfo.location(QLibraryInfo.LibraryExecutablesPath),
QLibraryInfo.location(QLibraryInfo.DataPath)
)
2016-07-06 23:47:59 +02:00
]
if not dist or dist.parsed == Distribution.unknown:
lines += _os_info()
lines += [
'',
2016-09-29 06:35:47 +02:00
'Paths:',
]
for name, path in sorted(_path_info().items()):
2016-09-27 13:22:28 +02:00
lines += ['{}: {}'.format(name, path)]
2017-11-14 03:17:44 +01:00
lines += [
'',
2017-11-14 06:19:42 +01:00
'Uptime: {}'.format(_uptime()),
2017-11-14 03:17:44 +01:00
]
2014-02-20 15:32:46 +01:00
return '\n'.join(lines)
def opengl_vendor(): # pragma: no cover
"""Get the OpenGL vendor used.
This returns a string such as 'nouveau' or
'Intel Open Source Technology Center'; or None if the vendor can't be
determined.
"""
assert QApplication.instance()
2018-07-19 00:30:12 +02:00
override = os.environ.get('QUTE_FAKE_OPENGL_VENDOR')
if override is not None:
log.init.debug("Using override {}".format(override))
return override
old_context = QOpenGLContext.currentContext()
old_surface = None if old_context is None else old_context.surface()
surface = QOffscreenSurface()
surface.create()
ctx = QOpenGLContext()
ok = ctx.create()
if not ok:
log.init.debug("Creating context failed!")
return None
ok = ctx.makeCurrent(surface)
if not ok:
log.init.debug("Making context current failed!")
return None
try:
if ctx.isOpenGLES():
# Can't use versionFunctions there
return None
vp = QOpenGLVersionProfile()
vp.setVersion(2, 0)
try:
vf = ctx.versionFunctions(vp)
except ImportError as e:
log.init.debug("Importing version functions failed: {}".format(e))
return None
if vf is None:
log.init.debug("Getting version functions failed!")
return None
return vf.glGetString(vf.GL_VENDOR)
finally:
ctx.doneCurrent()
if old_context and old_surface:
old_context.makeCurrent(old_surface)
2018-02-13 14:31:27 +01:00
def pastebin_version(pbclient=None):
"""Pastebin the version and log the url to messages."""
def _yank_url(url):
utils.set_clipboard(url)
message.info("Version url {} yanked to clipboard.".format(url))
def _on_paste_version_success(url):
2018-02-10 16:14:07 +01:00
global pastebin_url
url = url.strip()
_yank_url(url)
pbclient.deleteLater()
2018-02-10 16:14:07 +01:00
pastebin_url = url
def _on_paste_version_err(text):
message.error("Failed to pastebin version"
" info: {}".format(text))
pbclient.deleteLater()
2018-02-10 16:14:07 +01:00
if pastebin_url:
_yank_url(pastebin_url)
return
app = QApplication.instance()
http_client = httpclient.HTTPClient()
misc_api = pastebin.PastebinClient.MISC_API_URL
2018-02-13 14:31:27 +01:00
pbclient = pbclient or pastebin.PastebinClient(http_client, parent=app,
api_url=misc_api)
pbclient.success.connect(_on_paste_version_success)
pbclient.error.connect(_on_paste_version_err)
2018-02-07 23:55:42 +01:00
pbclient.paste(getpass.getuser(),
2018-02-08 15:01:51 +01:00
"qute version info {}".format(qutebrowser.__version__),
2018-02-26 23:09:55 +01:00
version(),
private=True)