diff options
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 |
commit | 317af23593584366a4eb1d7c077389162cb70c32 (patch) | |
tree | 553b3605cf89bd1919fda863757ede09561de3cb /scripts | |
parent | 6e5e80d39af5c258fb67a444d1a0a961474d1c05 (diff) | |
parent | 31cd414664ed8600a82c09aec75b13b28befeb5b (diff) | |
download | qutebrowser-317af23593584366a4eb1d7c077389162cb70c32.tar.gz qutebrowser-317af23593584366a4eb1d7c077389162cb70c32.zip |
Merge branch 'master' into more-sophisticated-adblock
Diffstat (limited to 'scripts')
-rwxr-xr-x | scripts/asciidoc2html.py | 8 | ||||
-rwxr-xr-x | scripts/dev/build_release.py | 18 | ||||
-rw-r--r-- | scripts/dev/check_coverage.py | 7 | ||||
-rw-r--r-- | scripts/dev/ci/docker/Dockerfile.j2 | 27 | ||||
-rw-r--r-- | scripts/dev/ci/docker/README.md | 9 | ||||
-rw-r--r-- | scripts/dev/ci/docker/generate.py | 45 | ||||
-rw-r--r-- | scripts/dev/ci/problemmatchers.py | 14 | ||||
-rw-r--r-- | scripts/dev/misc_checks.py | 128 | ||||
-rw-r--r-- | scripts/dev/recompile_requirements.py | 151 | ||||
-rwxr-xr-x | scripts/dev/run_vulture.py | 2 | ||||
-rwxr-xr-x | scripts/dictcli.py | 33 | ||||
-rw-r--r-- | scripts/mkvenv.py | 222 |
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() |