summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorÁrni Dagur <arni@dagur.eu>2020-12-19 20:30:03 +0000
committerÁrni Dagur <arni@dagur.eu>2020-12-19 20:30:03 +0000
commit317af23593584366a4eb1d7c077389162cb70c32 (patch)
tree553b3605cf89bd1919fda863757ede09561de3cb /scripts
parent6e5e80d39af5c258fb67a444d1a0a961474d1c05 (diff)
parent31cd414664ed8600a82c09aec75b13b28befeb5b (diff)
downloadqutebrowser-317af23593584366a4eb1d7c077389162cb70c32.tar.gz
qutebrowser-317af23593584366a4eb1d7c077389162cb70c32.zip
Merge branch 'master' into more-sophisticated-adblock
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/asciidoc2html.py8
-rwxr-xr-xscripts/dev/build_release.py18
-rw-r--r--scripts/dev/check_coverage.py7
-rw-r--r--scripts/dev/ci/docker/Dockerfile.j227
-rw-r--r--scripts/dev/ci/docker/README.md9
-rw-r--r--scripts/dev/ci/docker/generate.py45
-rw-r--r--scripts/dev/ci/problemmatchers.py14
-rw-r--r--scripts/dev/misc_checks.py128
-rw-r--r--scripts/dev/recompile_requirements.py151
-rwxr-xr-xscripts/dev/run_vulture.py2
-rwxr-xr-xscripts/dictcli.py33
-rw-r--r--scripts/mkvenv.py222
12 files changed, 511 insertions, 153 deletions
diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py
index 17c3fb367..109e61625 100755
--- a/scripts/asciidoc2html.py
+++ b/scripts/asciidoc2html.py
@@ -49,13 +49,13 @@ class AsciiDoc:
asciidoc: Optional[str],
asciidoc_python: Optional[str],
website: Optional[str]) -> None:
- self._cmd = None # type: Optional[List[str]]
+ self._cmd: Optional[List[str]] = None
self._asciidoc = asciidoc
self._asciidoc_python = asciidoc_python
self._website = website
- self._homedir = None # type: Optional[pathlib.Path]
- self._themedir = None # type: Optional[pathlib.Path]
- self._tempdir = None # type: Optional[pathlib.Path]
+ self._homedir: Optional[pathlib.Path] = None
+ self._themedir: Optional[pathlib.Path] = None
+ self._tempdir: Optional[pathlib.Path] = None
self._failed = False
def prepare(self) -> None:
diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py
index 8030d61a1..6044a1e18 100755
--- a/scripts/dev/build_release.py
+++ b/scripts/dev/build_release.py
@@ -252,7 +252,7 @@ def _get_windows_python_path(x64):
return fallback
-def build_windows():
+def build_windows(*, skip_packaging):
"""Build windows executables/setups."""
utils.print_title("Updating 3rdparty content")
update_3rdparty.run(nsis=True, ace=False, pdfjs=True, fancy_dmg=False)
@@ -289,6 +289,14 @@ def build_windows():
utils.print_title("Running 64bit smoke test")
smoke_test(os.path.join(out_64, 'qutebrowser.exe'))
+ if not skip_packaging:
+ artifacts += _package_windows(out_32, out_64)
+
+ return artifacts
+
+
+def _package_windows(out_32, out_64):
+ """Build installers/zips for Windows."""
utils.print_title("Building installers")
subprocess.run(['makensis.exe',
'/DVERSION={}'.format(qutebrowser.__version__),
@@ -301,7 +309,7 @@ def build_windows():
name_32 = 'qutebrowser-{}-win32.exe'.format(qutebrowser.__version__)
name_64 = 'qutebrowser-{}-amd64.exe'.format(qutebrowser.__version__)
- artifacts += [
+ artifacts = [
(os.path.join('dist', name_32),
'application/vnd.microsoft.portable-executable',
'Windows 32bit installer'),
@@ -465,7 +473,9 @@ def main():
"If not given, the current Python interpreter is used.",
nargs='?')
parser.add_argument('--upload', action='store_true', required=False,
- help="Toggle to upload the release to GitHub")
+ help="Toggle to upload the release to GitHub.")
+ parser.add_argument('--skip-packaging', action='store_true', required=False,
+ help="Skip Windows installer/zip generation.")
args = parser.parse_args()
utils.change_cwd()
@@ -487,7 +497,7 @@ def main():
run_asciidoc2html(args)
if os.name == 'nt':
- artifacts = build_windows()
+ artifacts = build_windows(skip_packaging=args.skip_packaging)
elif sys.platform == 'darwin':
artifacts = build_mac()
else:
diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py
index f4e4ac07f..728a36873 100644
--- a/scripts/dev/check_coverage.py
+++ b/scripts/dev/check_coverage.py
@@ -78,6 +78,8 @@ PERFECT_FILES = [
'qutebrowser/api/message.py'),
(None,
'qutebrowser/api/qtutils.py'),
+ (None,
+ 'qutebrowser/qt.py'),
('tests/unit/browser/webkit/test_cache.py',
'qutebrowser/browser/webkit/cache.py'),
@@ -331,8 +333,9 @@ def main_check():
subprocess.run([sys.executable, '-m', 'coverage', 'report',
'--show-missing', '--include', filters], check=True)
print()
- print("To debug this, run 'tox -e py36-pyqt59-cov' "
- "(or py35-pyqt59-cov) locally and check htmlcov/index.html")
+ print("To debug this, run 'tox -e py36-pyqt515-cov' "
+ "(replace Python/Qt versions based on your system) locally and check "
+ "htmlcov/index.html")
print("or check https://codecov.io/github/qutebrowser/qutebrowser")
print()
diff --git a/scripts/dev/ci/docker/Dockerfile.j2 b/scripts/dev/ci/docker/Dockerfile.j2
new file mode 100644
index 000000000..412c42cf2
--- /dev/null
+++ b/scripts/dev/ci/docker/Dockerfile.j2
@@ -0,0 +1,27 @@
+FROM thecompiler/archlinux
+MAINTAINER Florian Bruhin <me@the-compiler.org>
+
+{% if unstable %}
+RUN sed -i '/^# after the header/a[kde-unstable]\nInclude = /etc/pacman.d/mirrorlist\n\n[testing]\nInclude = /etc/pacman.d/mirrorlist' /etc/pacman.conf
+{% endif %}
+RUN pacman -Suyy --noconfirm \
+ git \
+ python-tox \
+ python-distlib \
+ qt5-base \
+ qt5-declarative \
+ {% if webengine %}qt5-webengine python-pyqtwebengine{% else %}qt5-webkit{% endif %} \
+ python-pyqt5 \
+ xorg-xinit \
+ xorg-server-xvfb \
+ ttf-bitstream-vera \
+ gcc \
+ libyaml \
+ xorg-xdpyinfo
+
+USER user
+WORKDIR /home/user
+
+CMD git clone /outside qutebrowser.git && \
+ cd qutebrowser.git && \
+ tox -e py38
diff --git a/scripts/dev/ci/docker/README.md b/scripts/dev/ci/docker/README.md
new file mode 100644
index 000000000..eb2b8db91
--- /dev/null
+++ b/scripts/dev/ci/docker/README.md
@@ -0,0 +1,9 @@
+This directory contains a Dockerfile template for containers used to test
+qutebrowser on CI.
+
+The `generate.py` script uses that template to generate various image
+configuration.
+
+The images are rebuilt via Github Actions in this directory, and qutebrowser
+then downloads them during the CI run. Note that means that it'll take a while
+until builds will use the newer image if you make a change to this directory.
diff --git a/scripts/dev/ci/docker/generate.py b/scripts/dev/ci/docker/generate.py
new file mode 100644
index 000000000..7d09fdb20
--- /dev/null
+++ b/scripts/dev/ci/docker/generate.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+# vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2019-2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# 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/>.
+
+"""Generate Dockerfiles for qutebrowser's CI."""
+
+import sys
+
+import jinja2
+
+
+def main():
+ with open('Dockerfile.j2') as f:
+ template = jinja2.Template(f.read())
+
+ image = sys.argv[1]
+ config = {
+ 'archlinux-webkit': {'webengine': False, 'unstable': False},
+ 'archlinux-webengine': {'webengine': True, 'unstable': False},
+ 'archlinux-webengine-unstable': {'webengine': True, 'unstable': True},
+ }[image]
+
+ with open('Dockerfile', 'w') as f:
+ f.write(template.render(**config))
+ f.write('\n')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/dev/ci/problemmatchers.py b/scripts/dev/ci/problemmatchers.py
index 320d0deeb..3e804af05 100644
--- a/scripts/dev/ci/problemmatchers.py
+++ b/scripts/dev/ci/problemmatchers.py
@@ -182,6 +182,20 @@ MATCHERS = {
],
},
],
+
+ "misc": [
+ {
+ "severity": "error",
+ "pattern": [
+ {
+ "regexp": r'^([^:]+):(\d+): (Found .*)',
+ "file": 1,
+ "line": 2,
+ "message": 3,
+ }
+ ]
+ }
+ ]
}
diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py
index df2845405..14373f94f 100644
--- a/scripts/dev/misc_checks.py
+++ b/scripts/dev/misc_checks.py
@@ -28,14 +28,14 @@ import argparse
import subprocess
import tokenize
import traceback
-import collections
import pathlib
from typing import List, Iterator, Optional
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
- os.pardir))
+REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
+sys.path.insert(0, str(REPO_ROOT))
from scripts import utils
+from scripts.dev import recompile_requirements
BINARY_EXTS = {'.png', '.icns', '.ico', '.bmp', '.gz', '.bin', '.pdf',
'.sqlite', '.woff2', '.whl'}
@@ -78,8 +78,46 @@ def _get_files(
yield path
+def check_changelog_urls(_args: argparse.Namespace = None) -> bool:
+ """Ensure we have changelog URLs for all requirements."""
+ ok = True
+ all_requirements = set()
+
+ for name in recompile_requirements.get_all_names():
+ outfile = recompile_requirements.get_outfile(name)
+ missing = set()
+ with open(outfile, 'r', encoding='utf-8') as f:
+ for line in f:
+ line = line.strip()
+ if line.startswith('#') or not line:
+ continue
+ req, _version = recompile_requirements.parse_versioned_line(line)
+ if req.startswith('./'):
+ continue
+ all_requirements.add(req)
+ if req not in recompile_requirements.CHANGELOG_URLS:
+ missing.add(req)
+
+ if missing:
+ ok = False
+ req_str = ', '.join(sorted(missing))
+ utils.print_col(
+ f"Missing changelog URLs in {name} requirements: {req_str}", 'red')
+
+ extra = set(recompile_requirements.CHANGELOG_URLS) - all_requirements
+ if extra:
+ ok = False
+ req_str = ', '.join(sorted(extra))
+ utils.print_col(f"Extra changelog URLs: {req_str}", 'red')
+
+ if not ok:
+ print("Hint: Changelog URLs are in scripts/dev/recompile_requirements.py")
+
+ return ok
+
+
def check_git(_args: argparse.Namespace = None) -> bool:
- """Check for uncommitted git files.."""
+ """Check for uncommitted git files."""
if not os.path.isdir(".git"):
print("No .git dir, ignoring")
print()
@@ -101,6 +139,17 @@ def check_git(_args: argparse.Namespace = None) -> bool:
return status
+def _check_spelling_file(path, fobj, patterns):
+ ok = True
+ for num, line in enumerate(fobj, start=1):
+ for pattern, explanation in patterns:
+ if pattern.search(line):
+ ok = False
+ print(f'{path}:{num}: Found "{pattern.pattern}" - ', end='')
+ utils.print_col(explanation, 'blue')
+ return ok
+
+
def check_spelling(args: argparse.Namespace) -> Optional[bool]:
"""Check commonly misspelled words."""
# Words which I often misspell
@@ -119,6 +168,37 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]:
'eventloops', 'sizehint', 'statemachine', 'metaobject',
'logrecord'}
+ patterns = [
+ (
+ re.compile(r'[{}{}]{}'.format(w[0], w[0].upper(), w[1:])),
+ "Common misspelling or non-US spelling"
+ ) for w in words
+ ]
+ patterns += [
+ (
+ re.compile(r'(?i)# noqa(?!: )'),
+ "Don't use a blanket 'noqa', use something like 'noqa: X123' instead.",
+ ),
+ (
+ re.compile(r'# type: ignore[^\[]'),
+ ("Don't use a blanket 'type: ignore', use something like "
+ "'type: ignore[error-code]' instead."),
+ ),
+ (
+ re.compile(r'# type: (?!ignore\[)'),
+ "Don't use type comments, use type annotations instead.",
+ ),
+ (
+ re.compile(r': typing\.'),
+ "Don't use typing.SomeType, do 'from typing import SomeType' instead.",
+ ),
+ (
+ re.compile(r"""monkeypatch\.setattr\(['"]"""),
+ "Don't use monkeypatch.setattr('obj.attr', value), use "
+ "setattr(obj, 'attr', value) instead.",
+ ),
+ ]
+
# Files which should be ignored, e.g. because they come from another
# package
hint_data = pathlib.Path('tests', 'end2end', 'data', 'hints')
@@ -129,20 +209,12 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]:
hint_data / 'bootstrap' / 'bootstrap.css',
]
- seen = collections.defaultdict(list)
try:
ok = True
for path in _get_files(verbose=args.verbose, ignored=ignored):
with tokenize.open(str(path)) as f:
- for line in f:
- for w in words:
- pattern = '[{}{}]{}'.format(w[0], w[0].upper(), w[1:])
- if (re.search(pattern, line) and
- path not in seen[w] and
- '# pragma: no spellcheck' not in line):
- print('Found "{}" in {}!'.format(w, path))
- seen[w].append(path)
- ok = False
+ if not _check_spelling_file(path, f, patterns):
+ ok = False
print()
return ok
except Exception:
@@ -203,20 +275,30 @@ def check_userscripts_descriptions(_args: argparse.Namespace = None) -> bool:
def main() -> int:
+ checkers = {
+ 'git': check_git,
+ 'vcs': check_vcs_conflict,
+ 'spelling': check_spelling,
+ 'userscripts': check_userscripts_descriptions,
+ 'changelog-urls': check_changelog_urls,
+ }
+
parser = argparse.ArgumentParser()
parser.add_argument('--verbose', action='store_true', help='Show checked filenames')
parser.add_argument('checker',
- choices=('git', 'vcs', 'spelling', 'userscripts'),
+ choices=list(checkers) + ['all'],
help="Which checker to run.")
args = parser.parse_args()
- if args.checker == 'git':
- ok = check_git(args)
- elif args.checker == 'vcs':
- ok = check_vcs_conflict(args)
- elif args.checker == 'spelling':
- ok = check_spelling(args)
- elif args.checker == 'userscripts':
- ok = check_userscripts_descriptions(args)
+
+ if args.checker == 'all':
+ retvals = []
+ for name, checker in checkers.items():
+ utils.print_title(name)
+ retvals.append(checker(args))
+ return 0 if all(retvals) else 1
+
+ checker = checkers[args.checker]
+ ok = checker(args)
return 0 if ok else 1
diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py
index 5b79b801d..094a69ebe 100644
--- a/scripts/dev/recompile_requirements.py
+++ b/scripts/dev/recompile_requirements.py
@@ -39,55 +39,88 @@ REQ_DIR = os.path.join(REPO_DIR, 'misc', 'requirements')
CHANGELOG_URLS = {
'pyparsing': 'https://github.com/pyparsing/pyparsing/blob/master/CHANGES',
- 'cherrypy': 'https://github.com/cherrypy/cherrypy/blob/master/CHANGES.rst',
'pylint': 'http://pylint.pycqa.org/en/latest/whatsnew/changelog.html',
- 'setuptools': 'https://github.com/pypa/setuptools/blob/master/CHANGES.rst',
+ 'isort': 'https://pycqa.github.io/isort/CHANGELOG/',
+ 'lazy-object-proxy': 'https://github.com/ionelmc/python-lazy-object-proxy/blob/master/CHANGELOG.rst',
+ 'mccabe': 'https://github.com/PyCQA/mccabe#changes',
'pytest-cov': 'https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst',
'pytest-xdist': 'https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst',
'pytest-forked': 'https://github.com/pytest-dev/pytest-forked/blob/master/CHANGELOG',
+ 'pytest-xvfb': 'https://github.com/The-Compiler/pytest-xvfb/blob/master/CHANGELOG.rst',
+ 'EasyProcess': 'https://github.com/ponty/EasyProcess/commits/master',
+ 'PyVirtualDisplay': 'https://github.com/ponty/PyVirtualDisplay/commits/master',
'execnet': 'https://execnet.readthedocs.io/en/latest/changelog.html',
'apipkg': 'https://github.com/pytest-dev/apipkg/blob/master/CHANGELOG',
'pytest-rerunfailures': 'https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst',
+ 'pytest-repeat': 'https://github.com/pytest-dev/pytest-repeat/blob/master/CHANGES.rst',
'requests': 'https://github.com/psf/requests/blob/master/HISTORY.md',
'requests-file': 'https://github.com/dashea/requests-file/blob/master/CHANGES.rst',
- 'werkzeug': 'https://github.com/pallets/werkzeug/blob/master/CHANGES.rst',
+ 'Werkzeug': 'https://github.com/pallets/werkzeug/blob/master/CHANGES.rst',
+ 'click': 'https://click.palletsprojects.com/en/7.x/changelog/',
+ 'itsdangerous': 'https://itsdangerous.palletsprojects.com/en/1.1.x/changes/',
+ 'parse-type': 'https://github.com/jenisys/parse_type/blob/master/CHANGES.txt',
+ 'sortedcontainers': 'https://github.com/grantjenks/python-sortedcontainers/blob/master/HISTORY.rst',
+ 'soupsieve': 'https://facelessuser.github.io/soupsieve/about/changelog/',
+ 'Flask': 'https://flask.palletsprojects.com/en/1.1.x/changelog/',
+ 'Mako': 'https://docs.makotemplates.org/en/latest/changelog.html',
+ 'glob2': 'https://github.com/miracle2k/python-glob2/blob/master/CHANGES',
'hypothesis': 'https://hypothesis.readthedocs.io/en/latest/changes.html',
'mypy': 'https://mypy-lang.blogspot.com/',
'pytest': 'https://docs.pytest.org/en/latest/changelog.html',
'iniconfig': 'https://github.com/RonnyPfannschmidt/iniconfig/blob/master/CHANGELOG',
'tox': 'https://tox.readthedocs.io/en/latest/changelog.html',
- 'pyyaml': 'https://github.com/yaml/pyyaml/blob/master/CHANGES',
+ 'PyYAML': 'https://github.com/yaml/pyyaml/blob/master/CHANGES',
'pytest-bdd': 'https://github.com/pytest-dev/pytest-bdd/blob/master/CHANGES.rst',
'snowballstemmer': 'https://github.com/snowballstem/snowball/blob/master/NEWS',
'virtualenv': 'https://virtualenv.pypa.io/en/latest/changelog.html',
- 'pip': 'https://pip.pypa.io/en/stable/news/',
'packaging': 'https://pypi.org/project/packaging/',
- 'flake8-docstrings': 'https://pypi.org/project/flake8-docstrings/',
+ 'build': 'https://github.com/pypa/build/commits/master',
'attrs': 'http://www.attrs.org/en/stable/changelog.html',
- 'jinja2': 'https://github.com/pallets/jinja/blob/master/CHANGES.rst',
+ 'Jinja2': 'https://github.com/pallets/jinja/blob/master/CHANGES.rst',
+ 'MarkupSafe': 'https://markupsafe.palletsprojects.com/en/1.1.x/changes/',
'flake8': 'https://gitlab.com/pycqa/flake8/tree/master/docs/source/release-notes',
- 'cffi': 'https://cffi.readthedocs.io/en/latest/whatsnew.html',
+ 'flake8-docstrings': 'https://pypi.org/project/flake8-docstrings/',
'flake8-debugger': 'https://github.com/JBKahn/flake8-debugger/',
+ 'flake8-builtins': 'https://github.com/gforcada/flake8-builtins/blob/master/CHANGES.rst',
+ 'flake8-bugbear': 'https://github.com/PyCQA/flake8-bugbear#change-log',
+ 'flake8-tidy-imports': 'https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst',
+ 'flake8-tuple': 'https://github.com/ar4s/flake8_tuple/blob/master/HISTORY.rst',
+ 'flake8-comprehensions': 'https://github.com/adamchainz/flake8-comprehensions/blob/master/HISTORY.rst',
+ 'flake8-copyright': 'https://github.com/savoirfairelinux/flake8-copyright/blob/master/CHANGELOG.rst',
+ 'flake8-deprecated': 'https://github.com/gforcada/flake8-deprecated/blob/master/CHANGES.rst',
+ 'flake8-future-import': 'https://github.com/xZise/flake8-future-import#changes',
+ 'flake8-mock': 'https://github.com/aleGpereira/flake8-mock#changes',
+ 'flake8-polyfill': 'https://gitlab.com/pycqa/flake8-polyfill/-/blob/master/CHANGELOG.rst',
+ 'flake8-string-format': 'https://github.com/xZise/flake8-string-format#changes',
+ 'pep8-naming': 'https://github.com/PyCQA/pep8-naming/blob/master/CHANGELOG.rst',
+ 'pycodestyle': 'https://github.com/PyCQA/pycodestyle/blob/master/CHANGES.txt',
+ 'pyflakes': 'https://github.com/PyCQA/pyflakes/blob/master/NEWS.rst',
+ 'cffi': 'https://cffi.readthedocs.io/en/latest/whatsnew.html',
'astroid': 'https://github.com/PyCQA/astroid/blob/2.4/ChangeLog',
'pytest-instafail': 'https://github.com/pytest-dev/pytest-instafail/blob/master/CHANGES.rst',
'coverage': 'https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst',
'colorama': 'https://github.com/tartley/colorama/blob/master/CHANGELOG.rst',
'hunter': 'https://github.com/ionelmc/python-hunter/blob/master/CHANGELOG.rst',
- 'uritemplate': 'https://pypi.org/project/uritemplate/',
- 'flake8-builtins': 'https://github.com/gforcada/flake8-builtins/blob/master/CHANGES.rst',
- 'flake8-bugbear': 'https://github.com/PyCQA/flake8-bugbear',
- 'flake8-tidy-imports': 'https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst',
- 'flake8-tuple': 'https://github.com/ar4s/flake8_tuple/blob/master/HISTORY.rst',
+ 'uritemplate': 'https://github.com/python-hyper/uritemplate/blob/master/HISTORY.rst',
'more-itertools': 'https://github.com/erikrose/more-itertools/blob/master/docs/versions.rst',
'pydocstyle': 'http://www.pydocstyle.org/en/latest/release_notes.html',
- 'sphinx': 'https://www.sphinx-doc.org/en/master/changes.html',
+ 'Sphinx': 'https://www.sphinx-doc.org/en/master/changes.html',
+ 'Babel': 'https://github.com/python-babel/babel/blob/master/CHANGES',
+ 'alabaster': 'https://alabaster.readthedocs.io/en/latest/changelog.html',
+ 'imagesize': 'https://github.com/shibukawa/imagesize_py/commits/master',
+ 'pytz': 'https://mm.icann.org/pipermail/tz-announce/',
+ 'sphinxcontrib-applehelp': 'https://www.sphinx-doc.org/en/master/changes.html',
+ 'sphinxcontrib-devhelp': 'https://www.sphinx-doc.org/en/master/changes.html',
+ 'sphinxcontrib-htmlhelp': 'https://www.sphinx-doc.org/en/master/changes.html',
+ 'sphinxcontrib-jsmath': 'https://www.sphinx-doc.org/en/master/changes.html',
+ 'sphinxcontrib-qthelp': 'https://www.sphinx-doc.org/en/master/changes.html',
+ 'sphinxcontrib-serializinghtml': 'https://www.sphinx-doc.org/en/master/changes.html',
'jaraco.functools': 'https://github.com/jaraco/jaraco.functools/blob/master/CHANGES.rst',
'parse': 'https://github.com/r1chardj0n3s/parse#potential-gotchas',
'py': 'https://py.readthedocs.io/en/latest/changelog.html#changelog',
'Pympler': 'https://github.com/pympler/pympler/blob/master/CHANGELOG.md',
'pytest-mock': 'https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst',
'pytest-qt': 'https://github.com/pytest-dev/pytest-qt/blob/master/CHANGELOG.rst',
- 'wcwidth': 'https://github.com/jquast/wcwidth#history',
'pyinstaller': 'https://pyinstaller.readthedocs.io/en/stable/CHANGES.html',
'pyinstaller-hooks-contrib': 'https://github.com/pyinstaller/pyinstaller-hooks-contrib/blob/master/CHANGELOG.rst',
'pytest-benchmark': 'https://pytest-benchmark.readthedocs.io/en/stable/changelog.html',
@@ -95,14 +128,10 @@ CHANGELOG_URLS = {
'docutils': 'https://docutils.sourceforge.io/RELEASE-NOTES.html',
'bump2version': 'https://github.com/c4urself/bump2version/blob/master/CHANGELOG.md',
'six': 'https://github.com/benjaminp/six/blob/master/CHANGES',
- 'flake8-comprehensions': 'https://github.com/adamchainz/flake8-comprehensions/blob/master/HISTORY.rst',
'altgraph': 'https://github.com/ronaldoussoren/altgraph/blob/master/doc/changelog.rst',
'urllib3': 'https://github.com/urllib3/urllib3/blob/master/CHANGES.rst',
- 'wheel': 'https://github.com/pypa/wheel/blob/master/docs/news.rst',
- 'mako': 'https://docs.makotemplates.org/en/latest/changelog.html',
'lxml': 'https://lxml.de/4.6/changes-4.6.0.html',
'jwcrypto': 'https://github.com/latchset/jwcrypto/commits/master',
- 'tox-pip-version': 'https://github.com/pglass/tox-pip-version/commits/master',
'wrapt': 'https://github.com/GrahamDumpleton/wrapt/blob/develop/docs/changes.rst',
'pep517': 'https://github.com/pypa/pep517/blob/master/doc/changelog.rst',
'cryptography': 'https://cryptography.io/en/latest/changelog.html',
@@ -122,8 +151,8 @@ CHANGELOG_URLS = {
'chardet': 'https://github.com/chardet/chardet/releases',
'idna': 'https://github.com/kjd/idna/blob/master/HISTORY.rst',
'tldextract': 'https://github.com/john-kurkowski/tldextract/blob/master/CHANGELOG.md',
- 'typing_extensions': 'https://github.com/python/typing/commits/master/typing_extensions',
- 'diff_cover': 'https://github.com/Bachmann1234/diff_cover/blob/master/CHANGELOG',
+ 'typing-extensions': 'https://github.com/python/typing/commits/master/typing_extensions',
+ 'diff-cover': 'https://github.com/Bachmann1234/diff_cover/blob/master/CHANGELOG',
'pytest-clarity': 'https://github.com/darrenburns/pytest-clarity/commits/master',
'pytest-icdiff': 'https://github.com/hjwp/pytest-icdiff/blob/master/HISTORY.rst',
'icdiff': 'https://github.com/jeffkaufman/icdiff/blob/master/ChangeLog',
@@ -132,12 +161,21 @@ CHANGELOG_URLS = {
'beautifulsoup4': 'https://bazaar.launchpad.net/~leonardr/beautifulsoup/bs4/view/head:/CHANGELOG',
'check-manifest': 'https://github.com/mgedmin/check-manifest/blob/master/CHANGES.rst',
'yamllint': 'https://github.com/adrienverge/yamllint/blob/master/CHANGELOG.rst',
+ 'pathspec': 'https://github.com/cpburnz/python-path-specification/blob/master/CHANGES.rst',
'filelock': 'https://github.com/benediktschmitt/py-filelock/commits/master',
+ 'github3.py': 'https://github3py.readthedocs.io/en/master/release-notes/index.html',
+ 'manhole': 'https://github3py.readthedocs.io/en/master/release-notes/index.html',
+ 'pycparser': 'https://github.com/eliben/pycparser/blob/master/CHANGES',
+ 'python-dateutil': 'https://dateutil.readthedocs.io/en/stable/changelog.html',
+ 'appdirs': 'https://github.com/ActiveState/appdirs/blob/master/CHANGES.rst',
+ 'pluggy': 'https://github.com/pytest-dev/pluggy/blob/master/CHANGELOG.rst',
+ 'inflect': 'https://github.com/jazzband/inflect/blob/master/CHANGES.rst',
+ 'jinja2-pluralize': 'https://github.com/audreyfeldroy/jinja2_pluralize/blob/master/HISTORY.rst',
+ 'mypy-extensions': 'https://github.com/python/mypy_extensions/commits/master',
+ 'pyroma': 'https://github.com/regebro/pyroma/blob/master/HISTORY.txt',
+ 'pyPEG2': None,
}
-# PyQt versions which need SIP v4
-OLD_PYQT = {'pyqt-5.7', 'pyqt-5.9', 'pyqt-5.10', 'pyqt-5.11'}
-
def convert_line(line, comments):
"""Convert the given requirement line to place into the output."""
@@ -223,14 +261,6 @@ def get_all_names():
yield basename[len('requirements-'):-len('.txt-raw')]
-def filter_names(names, old_pyqt=False):
- """Filter requirement names."""
- if old_pyqt:
- return sorted(names)
- else:
- return sorted(set(names) - OLD_PYQT)
-
-
def run_pip(venv_dir, *args, **kwargs):
"""Run pip inside the virtualenv."""
arg_str = ' '.join(str(arg) for arg in args)
@@ -261,9 +291,6 @@ def init_venv(host_python, venv_dir, requirements, pre=False):
def parse_args():
"""Parse commandline arguments via argparse."""
parser = argparse.ArgumentParser()
- parser.add_argument('--old-pyqt',
- action='store_true',
- help='Also include old PyQt requirements.')
parser.add_argument('names', nargs='*')
return parser.parse_args()
@@ -287,7 +314,7 @@ class Change:
self.name = name
self.old = None
self.new = None
- if name in CHANGELOG_URLS:
+ if CHANGELOG_URLS.get(name):
self.url = CHANGELOG_URLS[name]
self.link = '[{}]({})'.format(self.name, self.url)
else:
@@ -327,6 +354,25 @@ def _get_changed_files():
return sorted(changed_files)
+def parse_versioned_line(line):
+ """Parse a requirements.txt line into name/version."""
+ if '==' in line:
+ name, version = line.split('==')
+ if ';' in version: # pip environment markers
+ version = version.split(';')[0].strip()
+ elif line.startswith('-e'):
+ rest, name = line.split('#egg=')
+ version = rest.split('@')[1][:7]
+ else:
+ name = line
+ version = '?'
+
+ if name.startswith('#'): # duplicate requirements
+ name = name[1:].strip()
+
+ return name, version
+
+
def _get_changes():
"""Get a list of changed versions from git."""
changes_dict = {}
@@ -337,19 +383,7 @@ def _get_changes():
if line.startswith('+++ ') or line.startswith('--- '):
continue
- if '==' in line:
- name, version = line[1:].split('==')
- if ';' in version: # pip environment markers
- version = version.split(';')[0].strip()
- elif line[1:].startswith('-e'):
- rest, name = line.split('#egg=')
- version = rest.split('@')[1][:7]
- else:
- name = line[1:]
- version = '?'
-
- if name.startswith('#'): # duplicate requirements
- name = name[1:].strip()
+ name, version = parse_versioned_line(line[1:])
if name not in changes_dict:
changes_dict[name] = Change(name)
@@ -393,15 +427,21 @@ def print_changed_files():
def get_host_python(name):
"""Get the Python to use for a given requirement name.
- Old PyQt versions need sip v4 which doesn't work on Python 3.8
- ylint installs typed_ast on < 3.8 only
+ pylint installs typed_ast on < 3.8 only
"""
- if name in OLD_PYQT or name == 'pylint':
+ if name == 'pylint':
return 'python3.7'
else:
return sys.executable
+def get_outfile(name):
+ """Get the path to the output requirements.txt file."""
+ if name == 'qutebrowser':
+ return os.path.join(REPO_DIR, 'requirements.txt')
+ return os.path.join(REQ_DIR, 'requirements-{}.txt'.format(name))
+
+
def build_requirements(name):
"""Build a requirements file."""
utils.print_subtitle("Building")
@@ -422,10 +462,7 @@ def build_requirements(name):
if utils.ON_CI:
print(reqs.strip())
- if name == 'qutebrowser':
- outfile = os.path.join(REPO_DIR, 'requirements.txt')
- else:
- outfile = os.path.join(REQ_DIR, 'requirements-{}.txt'.format(name))
+ outfile = get_outfile(name)
with open(outfile, 'w', encoding='utf-8') as f:
f.write("# This file is automatically generated by "
@@ -484,7 +521,7 @@ def main():
if args.names:
names = args.names
else:
- names = filter_names(get_all_names(), old_pyqt=args.old_pyqt)
+ names = sorted(get_all_names())
utils.print_col('Rebuilding requirements: ' + ', '.join(names), 'green')
for name in names:
diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py
index 1d024c6e5..194381421 100755
--- a/scripts/dev/run_vulture.py
+++ b/scripts/dev/run_vulture.py
@@ -42,7 +42,7 @@ from qutebrowser.browser import qutescheme
from qutebrowser.config import configtypes
-def whitelist_generator(): # noqa
+def whitelist_generator(): # noqa: C901
"""Generator which yields lines to add to a vulture whitelist."""
loader.load_components(skip_hooks=True)
diff --git a/scripts/dictcli.py b/scripts/dictcli.py
index ebe4e285c..4e38727dd 100755
--- a/scripts/dictcli.py
+++ b/scripts/dictcli.py
@@ -37,8 +37,7 @@ import attr
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from qutebrowser.browser.webengine import spell
from qutebrowser.config import configdata
-from qutebrowser.utils import standarddir, utils
-from scripts import utils as scriptutils
+from qutebrowser.utils import standarddir
API_URL = 'https://chromium.googlesource.com/chromium/deps/hunspell_dictionaries.git/+/master/'
@@ -216,17 +215,8 @@ def install_lang(lang):
def install(languages):
"""Install languages."""
for lang in languages:
- try:
- print('Installing {}: {}'.format(lang.code, lang.name))
- install_lang(lang)
- except PermissionError as e:
- msg = ("\n{}\n\nWith Qt < 5.10, you will need to run this script "
- "as root, as dictionaries need to be installed "
- "system-wide. If your qutebrowser uses a newer Qt version "
- "via a virtualenv, make sure you start this script with "
- "the virtualenv's Python.".format(e))
- scriptutils.print_error(msg)
- sys.exit(1)
+ print('Installing {}: {}'.format(lang.code, lang.name))
+ install_lang(lang)
def update(languages):
@@ -250,24 +240,7 @@ def remove_old(languages):
os.remove(os.path.join(spell.dictionary_dir(), old_file))
-def check_root():
- """Ask for confirmation if running as root when unnecessary."""
- if not utils.is_posix:
- return
-
- if spell.can_use_data_path() and os.geteuid() == 0:
- print("You're running Qt >= 5.10 which means qutebrowser will "
- "load dictionaries from a path in your home-directory. "
- "Unless you run qutebrowser as root (bad idea!), you "
- "most likely want to run this script as your user. ")
- answer = input("Do you want to continue anyways? [y/N] ")
- if answer not in ['y', 'Y']:
- sys.exit(0)
-
-
def main():
- check_root()
-
if configdata.DATA is None:
configdata.init()
standarddir.init(None)
diff --git a/scripts/mkvenv.py b/scripts/mkvenv.py
index 5eeb90640..ad5f2073e 100644
--- a/scripts/mkvenv.py
+++ b/scripts/mkvenv.py
@@ -24,12 +24,13 @@
import argparse
import pathlib
import sys
+import re
import os
import os.path
-import typing
import shutil
-import venv
+import venv as pyvenv
import subprocess
+from typing import List, Optional, Tuple, Dict, Union
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from scripts import utils, link_pyqt
@@ -38,7 +39,22 @@ from scripts import utils, link_pyqt
REPO_ROOT = pathlib.Path(__file__).parent.parent
-def parse_args() -> argparse.Namespace:
+class Error(Exception):
+
+ """Exception for errors in this script."""
+
+ def __init__(self, msg, code=1):
+ super().__init__(msg)
+ self.code = code
+
+
+def print_command(*cmd: Union[str, pathlib.Path], venv: bool) -> None:
+ """Print a command being run."""
+ prefix = 'venv$ ' if venv else '$ '
+ utils.print_col(prefix + ' '.join([str(e) for e in cmd]), 'blue')
+
+
+def parse_args(argv: List[str] = None) -> argparse.Namespace:
"""Parse commandline arguments."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--keep',
@@ -74,10 +90,10 @@ def parse_args() -> argparse.Namespace:
parser.add_argument('--tox-error',
action='store_true',
help=argparse.SUPPRESS)
- return parser.parse_args()
+ return parser.parse_args(argv)
-def pyqt_versions() -> typing.List[str]:
+def pyqt_versions() -> List[str]:
"""Get a list of all available PyQt versions.
The list is based on the filenames of misc/requirements/ files.
@@ -93,22 +109,40 @@ def pyqt_versions() -> typing.List[str]:
return versions + ['auto']
-def run_venv(venv_dir: pathlib.Path, executable, *args: str) -> None:
+def run_venv(
+ venv_dir: pathlib.Path,
+ executable,
+ *args: str,
+ capture_output=False,
+ capture_error=False,
+ env=None,
+) -> subprocess.CompletedProcess:
"""Run the given command inside the virtualenv."""
subdir = 'Scripts' if os.name == 'nt' else 'bin'
+ if env is None:
+ proc_env = None
+ else:
+ proc_env = os.environ.copy()
+ proc_env.update(env)
+
try:
- subprocess.run([str(venv_dir / subdir / executable)] +
- [str(arg) for arg in args], check=True)
+ return subprocess.run(
+ [str(venv_dir / subdir / executable)] + [str(arg) for arg in args],
+ check=True,
+ universal_newlines=capture_output or capture_error,
+ stdout=subprocess.PIPE if capture_output else None,
+ stderr=subprocess.PIPE if capture_error else None,
+ env=proc_env,
+ )
except subprocess.CalledProcessError as e:
- utils.print_error("Subprocess failed, exiting")
- sys.exit(e.returncode)
+ raise Error("Subprocess failed, exiting") from e
def pip_install(venv_dir: pathlib.Path, *args: str) -> None:
"""Run a pip install command inside the virtualenv."""
arg_str = ' '.join(str(arg) for arg in args)
- utils.print_col('venv$ pip install {}'.format(arg_str), 'blue')
+ print_command('pip install', arg_str, venv=True)
run_venv(venv_dir, 'python', '-m', 'pip', 'install', *args)
@@ -125,27 +159,25 @@ def delete_old_venv(venv_dir: pathlib.Path) -> None:
]
if not any(m.exists() for m in markers):
- utils.print_error('{} does not look like a virtualenv, '
- 'cowardly refusing to remove it.'.format(venv_dir))
- sys.exit(1)
+ raise Error('{} does not look like a virtualenv, cowardly refusing to '
+ 'remove it.'.format(venv_dir))
- utils.print_col('$ rm -r {}'.format(venv_dir), 'blue')
+ print_command('rm -r', venv_dir, venv=False)
shutil.rmtree(str(venv_dir))
def create_venv(venv_dir: pathlib.Path, use_virtualenv: bool = False) -> None:
"""Create a new virtualenv."""
if use_virtualenv:
- utils.print_col('$ python3 -m virtualenv {}'.format(venv_dir), 'blue')
+ print_command('python3 -m virtualenv', venv_dir, venv=False)
try:
subprocess.run([sys.executable, '-m', 'virtualenv', venv_dir],
check=True)
except subprocess.CalledProcessError as e:
- utils.print_error("virtualenv failed, exiting")
- sys.exit(e.returncode)
+ raise Error("virtualenv failed, exiting", e.returncode)
else:
- utils.print_col('$ python3 -m venv {}'.format(venv_dir), 'blue')
- venv.create(str(venv_dir), with_pip=True)
+ print_command('python3 -m venv', venv_dir, venv=False)
+ pyvenv.create(str(venv_dir), with_pip=True)
def upgrade_seed_pkgs(venv_dir: pathlib.Path) -> None:
@@ -202,6 +234,122 @@ def install_pyqt_wheels(venv_dir: pathlib.Path,
pip_install(venv_dir, *wheels)
+def apply_xcb_util_workaround(
+ venv_dir: pathlib.Path,
+ pyqt_type: str,
+ pyqt_version: str,
+) -> None:
+ """If needed (Debian Stable), symlink libxcb-util.so.0 -> .1.
+
+ WORKAROUND for https://bugreports.qt.io/browse/QTBUG-88688
+ """
+ utils.print_title("Running xcb-util workaround")
+
+ if not sys.platform.startswith('linux'):
+ print("Workaround not needed: Not on Linux.")
+ return
+ if pyqt_type != 'binary':
+ print("Workaround not needed: Not installing from PyQt binaries.")
+ return
+ if pyqt_version not in ['auto', '5.15']:
+ print("Workaround not needed: Not installing Qt 5.15.")
+ return
+
+ libs = _find_libs()
+ abi_type = 'libc6,x86-64' # the only one PyQt wheels are available for
+
+ if ('libxcb-util.so.1', abi_type) in libs:
+ print("Workaround not needed: libxcb-util.so.1 found.")
+ return
+
+ try:
+ libxcb_util_libs = libs['libxcb-util.so.0', abi_type]
+ except KeyError:
+ utils.print_error('Workaround failed: libxcb-util.so.0 not found.')
+ return
+
+ if len(libxcb_util_libs) > 1:
+ utils.print_error(
+ f'Workaround failed: Multiple matching libxcb-util found: '
+ f'{libxcb_util_libs}')
+ return
+
+ libxcb_util_path = pathlib.Path(libxcb_util_libs[0])
+
+ code = [
+ 'from PyQt5.QtCore import QLibraryInfo',
+ 'print(QLibraryInfo.location(QLibraryInfo.LibrariesPath))',
+ ]
+ proc = run_venv(venv_dir, 'python', '-c', '; '.join(code), capture_output=True)
+ venv_lib_path = pathlib.Path(proc.stdout.strip())
+
+ link_path = venv_lib_path / libxcb_util_path.with_suffix('.1').name
+
+ # This gives us a nicer path to print, and also conveniently makes sure we
+ # didn't accidentally end up with a path outside the venv.
+ rel_link_path = venv_dir / link_path.relative_to(venv_dir.resolve())
+ print_command('ln -s', libxcb_util_path, rel_link_path, venv=False)
+
+ link_path.symlink_to(libxcb_util_path)
+
+
+def _find_libs() -> Dict[Tuple[str, str], List[str]]:
+ """Find all system-wide .so libraries."""
+ all_libs: Dict[Tuple[str, str], List[str]] = {}
+ ldconfig_proc = subprocess.run(
+ ['ldconfig', '-p'],
+ check=True,
+ stdout=subprocess.PIPE,
+ encoding=sys.getfilesystemencoding(),
+ )
+ pattern = re.compile(r'(?P<name>\S+) \((?P<abi_type>[^)]+)\) => (?P<path>.*)')
+ for line in ldconfig_proc.stdout.splitlines():
+ match = pattern.fullmatch(line.strip())
+ if match is None:
+ if 'libs found in cache' not in line:
+ utils.print_col(f'Failed to match ldconfig output: {line}', 'yellow')
+ continue
+
+ key = match.group('name'), match.group('abi_type')
+ path = match.group('path')
+
+ libs = all_libs.setdefault(key, [])
+ libs.append(path)
+
+ return all_libs
+
+
+def run_qt_smoke_test(venv_dir: pathlib.Path) -> None:
+ """Make sure the Qt installation works."""
+ utils.print_title("Running Qt smoke test")
+ code = [
+ 'import sys',
+ 'from PyQt5.QtWidgets import QApplication',
+ 'from PyQt5.QtCore import qVersion, QT_VERSION_STR, PYQT_VERSION_STR',
+ 'print(f"Python: {sys.version}")',
+ 'print(f"qVersion: {qVersion()}")',
+ 'print(f"QT_VERSION_STR: {QT_VERSION_STR}")',
+ 'print(f"PYQT_VERSION_STR: {PYQT_VERSION_STR}")',
+ 'QApplication([])',
+ 'print("Qt seems to work properly!")',
+ 'print()',
+ ]
+ try:
+ run_venv(
+ venv_dir,
+ 'python', '-c', '; '.join(code),
+ env={'QT_DEBUG_PLUGINS': '1'},
+ capture_error=True
+ )
+ except Error as e:
+ proc_e = e.__cause__
+ assert isinstance(proc_e, subprocess.CalledProcessError), proc_e
+ print(proc_e.stderr)
+ raise Error(
+ f"Smoke test failed with status {proc_e.returncode}. "
+ "You might find additional information in the debug output above.")
+
+
def install_requirements(venv_dir: pathlib.Path) -> None:
"""Install qutebrowser's requirement.txt."""
utils.print_title("Installing other qutebrowser dependencies")
@@ -224,7 +372,7 @@ def install_qutebrowser(venv_dir: pathlib.Path) -> None:
def regenerate_docs(venv_dir: pathlib.Path,
- asciidoc: typing.Optional[typing.Tuple[str, str]]):
+ asciidoc: Optional[Tuple[str, str]]):
"""Regenerate docs using asciidoc."""
utils.print_title("Generating documentation")
if asciidoc is not None:
@@ -233,27 +381,24 @@ def regenerate_docs(venv_dir: pathlib.Path,
a2h_args = []
script_path = pathlib.Path(__file__).parent / 'asciidoc2html.py'
- utils.print_col('venv$ python3 scripts/asciidoc2html.py {}'
- .format(' '.join(a2h_args)), 'blue')
+ print_command('python3 scripts/asciidoc2html.py', *a2h_args, venv=True)
run_venv(venv_dir, 'python', str(script_path), *a2h_args)
-def main() -> None:
+def run(args) -> None:
"""Install qutebrowser in a virtualenv.."""
- args = parse_args()
venv_dir = pathlib.Path(args.venv_dir)
wheels_dir = pathlib.Path(args.pyqt_wheels_dir)
utils.change_cwd()
if (args.pyqt_version != 'auto' and
args.pyqt_type not in ['binary', 'source']):
- utils.print_error('The --pyqt-version option is only available when '
- 'installing PyQt from binary or source')
- sys.exit(1)
- elif args.pyqt_wheels_dir != 'wheels' and args.pyqt_type != 'wheels':
- utils.print_error('The --pyqt-wheels-dir option is only available '
- 'when installing PyQt from wheels')
- sys.exit(1)
+ raise Error('The --pyqt-version option is only available when installing PyQt '
+ 'from binary or source')
+
+ if args.pyqt_wheels_dir != 'wheels' and args.pyqt_type != 'wheels':
+ raise Error('The --pyqt-wheels-dir option is only available when installing '
+ 'PyQt from wheels')
if not args.keep:
utils.print_title("Creating virtual environment")
@@ -275,6 +420,10 @@ def main() -> None:
else:
raise AssertionError
+ apply_xcb_util_workaround(venv_dir, args.pyqt_type, args.pyqt_version)
+ if args.pyqt_type != 'skip':
+ run_qt_smoke_test(venv_dir)
+
install_requirements(venv_dir)
install_qutebrowser(venv_dir)
if args.dev:
@@ -284,5 +433,14 @@ def main() -> None:
regenerate_docs(venv_dir, args.asciidoc)
+def main():
+ args = parse_args()
+ try:
+ run(args)
+ except Error as e:
+ utils.print_error(str(e))
+ sys.exit(e.code)
+
+
if __name__ == '__main__':
main()