summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorÁrni Dagur <agudmundsson@fc-md.umd.edu>2020-08-03 01:50:09 +0000
committerÁrni Dagur <arni@dagur.eu>2020-12-19 20:24:06 +0000
commit68b9960a67158dceb7a50f4152074abf9696046b (patch)
treef52599e81a4b807dcf5f517bcdd177efd916386d /scripts
parent36831af853e7df59c55f07005ded015b47c5e4e1 (diff)
parentc04ab823a84b974fd26f5bbb1f9e6a6a175c038a (diff)
downloadqutebrowser-68b9960a67158dceb7a50f4152074abf9696046b.tar.gz
qutebrowser-68b9960a67158dceb7a50f4152074abf9696046b.zip
Merge branch 'master' into more-sophisticated-adblock
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/asciidoc2html.py80
-rwxr-xr-xscripts/dev/build_release.py13
-rw-r--r--scripts/dev/check_coverage.py11
-rwxr-xr-xscripts/dev/check_doc_changes.py20
-rw-r--r--scripts/dev/ci/backtrace.sh (renamed from scripts/dev/ci/travis_backtrace.sh)8
-rw-r--r--scripts/dev/ci/problemmatchers.py214
-rw-r--r--scripts/dev/ci/travis_install.sh75
-rw-r--r--scripts/dev/ci/travis_run.sh37
-rw-r--r--scripts/dev/misc_checks.py29
-rw-r--r--scripts/dev/recompile_requirements.py372
-rw-r--r--scripts/dev/run_shellcheck.sh39
-rw-r--r--scripts/dev/update_version.py6
-rw-r--r--scripts/utils.py31
13 files changed, 690 insertions, 245 deletions
diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py
index e7e7f680b..5cb49c767 100755
--- a/scripts/asciidoc2html.py
+++ b/scripts/asciidoc2html.py
@@ -23,7 +23,6 @@
from typing import List, Optional
import re
import os
-import os.path
import sys
import subprocess
import shutil
@@ -32,11 +31,12 @@ import argparse
import io
import pathlib
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
+REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
+DOC_DIR = REPO_ROOT / 'qutebrowser' / 'html' / 'doc'
-from scripts import utils
+sys.path.insert(0, str(REPO_ROOT))
-DOC_DIR = pathlib.Path("qutebrowser/html/doc")
+from scripts import utils
class AsciiDoc:
@@ -68,7 +68,7 @@ class AsciiDoc:
def cleanup(self) -> None:
"""Clean up the temporary home directory for asciidoc."""
if self._homedir is not None and not self._failed:
- shutil.rmtree(self._homedir)
+ shutil.rmtree(str(self._homedir))
def build(self) -> None:
"""Build either the website or the docs."""
@@ -80,9 +80,9 @@ class AsciiDoc:
def _build_docs(self) -> None:
"""Render .asciidoc files to .html sites."""
- files = [(pathlib.Path('doc/{}.asciidoc'.format(f)),
+ files = [((REPO_ROOT / 'doc' / '{}.asciidoc'.format(f)),
DOC_DIR / (f + ".html")) for f in self.FILES]
- for src in pathlib.Path('doc/help/').glob('*.asciidoc'):
+ for src in (REPO_ROOT / 'doc' / 'help').glob('*.asciidoc'):
dst = DOC_DIR / (src.stem + ".html")
files.append((src, dst))
@@ -98,12 +98,12 @@ class AsciiDoc:
for src, dst in files:
assert self._tempdir is not None # for mypy
modified_src = self._tempdir / src.name
- with open(modified_src, 'w', encoding='utf-8') as modified_f, \
- open(src, 'r', encoding='utf-8') as f:
+ with modified_src.open('w', encoding='utf-8') as moded_f, \
+ src.open('r', encoding='utf-8') as f:
for line in f:
for orig, repl in replacements:
line = line.replace(orig, repl)
- modified_f.write(line)
+ moded_f.write(line)
self.call(modified_src, dst, *asciidoc_args)
def _copy_images(self) -> None:
@@ -112,29 +112,28 @@ class AsciiDoc:
dst_path = DOC_DIR / 'img'
dst_path.mkdir(exist_ok=True)
for filename in ['cheatsheet-big.png', 'cheatsheet-small.png']:
- src = pathlib.Path('doc') / 'img' / filename
+ src = REPO_ROOT / 'doc' / 'img' / filename
dst = dst_path / filename
- shutil.copy(src, dst)
+ shutil.copy(str(src), str(dst))
def _build_website_file(self, root: pathlib.Path, filename: str) -> None:
"""Build a single website file."""
src = root / filename
assert self._website is not None # for mypy
dst = pathlib.Path(self._website)
- dst = dst / src.parent.relative_to('.') / (src.stem + ".html")
+ dst = dst / src.parent.relative_to(REPO_ROOT) / (src.stem + ".html")
dst.parent.mkdir(exist_ok=True)
assert self._tempdir is not None # for mypy
modified_src = self._tempdir / src.name
- shutil.copy('www/header.asciidoc', modified_src)
+ shutil.copy(str(REPO_ROOT / 'www' / 'header.asciidoc'), modified_src)
outfp = io.StringIO()
- with open(modified_src, 'r', encoding='utf-8') as header_file:
- header = header_file.read()
- header += "\n\n"
+ header = modified_src.read_text(encoding='utf-8')
+ header += "\n\n"
- with open(src, 'r', encoding='utf-8') as infp:
+ with src.open('r', encoding='utf-8') as infp:
outfp.write("\n\n")
hidden = False
found_title = False
@@ -174,8 +173,8 @@ class AsciiDoc:
current_lines = outfp.getvalue()
outfp.close()
- with open(modified_src, 'w+', encoding='utf-8') as final_version:
- final_version.write(title + "\n\n" + header + current_lines)
+ modified_str = title + "\n\n" + header + current_lines
+ modified_src.write_text(modified_str, encoding='utf-8')
asciidoc_args = ['--theme=qute', '-a toc', '-a toc-placement=manual',
'-a', 'source-highlighter=pygments']
@@ -183,14 +182,14 @@ class AsciiDoc:
def _build_website(self) -> None:
"""Prepare and build the website."""
- theme_file = (pathlib.Path('www') / 'qute.css').resolve()
+ theme_file = REPO_ROOT / 'www' / 'qute.css'
assert self._themedir is not None # for mypy
shutil.copy(theme_file, self._themedir)
assert self._website is not None # for mypy
outdir = pathlib.Path(self._website)
- for item_path in pathlib.Path().rglob('*.asciidoc'):
+ for item_path in pathlib.Path(REPO_ROOT).rglob('*.asciidoc'):
if item_path.stem in ['header', 'OpenSans-License']:
continue
self._build_website_file(item_path.parent, item_path.name)
@@ -198,17 +197,18 @@ class AsciiDoc:
copy = {'icons': 'icons', 'doc/img': 'doc/img', 'www/media': 'media/'}
for src, dest in copy.items():
+ full_src = REPO_ROOT / src
full_dest = outdir / dest
try:
shutil.rmtree(full_dest)
except FileNotFoundError:
pass
- shutil.copytree(src, full_dest)
+ shutil.copytree(full_src, full_dest)
for dst, link_name in [
('README.html', 'index.html'),
- ((pathlib.Path('doc') / 'quickstart.html'),
- 'quickstart.html')]:
+ ((pathlib.Path('doc') / 'quickstart.html'), 'quickstart.html'),
+ ]:
assert isinstance(dst, (str, pathlib.Path)) # for mypy
try:
(outdir / link_name).symlink_to(dst)
@@ -220,21 +220,16 @@ class AsciiDoc:
if self._asciidoc is not None:
return self._asciidoc
- try:
- subprocess.run(['asciidoc'], stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL, check=True)
- except OSError:
- pass
- else:
- return ['asciidoc']
-
- try:
- subprocess.run(['asciidoc.py'], stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL, check=True)
- except OSError:
- pass
- else:
- return ['asciidoc.py']
+ for executable in ['asciidoc', 'asciidoc.py']:
+ try:
+ subprocess.run([executable, '--version'],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ check=True)
+ except OSError:
+ pass
+ else:
+ return [executable]
raise FileNotFoundError
@@ -253,9 +248,14 @@ class AsciiDoc:
cmdline += ['--out-file', str(dst)]
cmdline += args
cmdline.append(str(src))
+
+ # So the virtualenv's Pygments is found
+ bin_path = pathlib.Path(sys.executable).parent
+
try:
env = os.environ.copy()
env['HOME'] = str(self._homedir)
+ env['PATH'] = str(bin_path) + os.pathsep + env['PATH']
subprocess.run(cmdline, check=True, env=env)
except (subprocess.CalledProcessError, OSError) as e:
self._failed = True
diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py
index 2f037ac68..ee0ac2c53 100755
--- a/scripts/dev/build_release.py
+++ b/scripts/dev/build_release.py
@@ -275,12 +275,12 @@ def build_windows():
utils.print_title("Running pyinstaller 32bit")
_maybe_remove(out_32)
- call_tox('pyinstaller32', '-r', python=python_x86)
+ call_tox('pyinstaller-32', '-r', python=python_x86)
shutil.move(out_pyinstaller, out_32)
utils.print_title("Running pyinstaller 64bit")
_maybe_remove(out_64)
- call_tox('pyinstaller', '-r', python=python_x64)
+ call_tox('pyinstaller-64', '-r', python=python_x64)
shutil.move(out_pyinstaller, out_64)
utils.print_title("Running 32bit smoke test")
@@ -310,16 +310,17 @@ def build_windows():
]
utils.print_title("Zipping 32bit standalone...")
- name = 'qutebrowser-{}-windows-standalone-win32'.format(
- qutebrowser.__version__)
+ template = 'qutebrowser-{}-windows-standalone-{}'
+ name = os.path.join('dist',
+ template.format(qutebrowser.__version__, 'win32'))
shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_32))
artifacts.append(('{}.zip'.format(name),
'application/zip',
'Windows 32bit standalone'))
utils.print_title("Zipping 64bit standalone...")
- name = 'qutebrowser-{}-windows-standalone-amd64'.format(
- qutebrowser.__version__)
+ name = os.path.join('dist',
+ template.format(qutebrowser.__version__, 'amd64'))
shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_64))
artifacts.append(('{}.zip'.format(name),
'application/zip',
diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py
index 7fa45dd90..12963de38 100644
--- a/scripts/dev/check_coverage.py
+++ b/scripts/dev/check_coverage.py
@@ -45,6 +45,13 @@ class Message:
filename = attr.ib()
text = attr.ib()
+ def show(self):
+ """Print this message."""
+ if scriptutils.ON_CI:
+ scriptutils.gha_error(self.text)
+ else:
+ print(self.text)
+
MsgType = enum.Enum('MsgType', 'insufficient_coverage, perfect_file')
@@ -159,6 +166,8 @@ PERFECT_FILES = [
'config/configtypes.py'),
('tests/unit/config/test_configinit.py',
'config/configinit.py'),
+ ('tests/unit/config/test_qtargs.py',
+ 'config/qtargs.py'),
('tests/unit/config/test_configcommands.py',
'config/configcommands.py'),
('tests/unit/config/test_configutils.py',
@@ -309,7 +318,7 @@ def main_check():
print()
scriptutils.print_title("Coverage check failed")
for msg in messages:
- print(msg.text)
+ msg.show()
print()
filters = ','.join('qutebrowser/' + msg.filename for msg in messages)
subprocess.run([sys.executable, '-m', 'coverage', 'report',
diff --git a/scripts/dev/check_doc_changes.py b/scripts/dev/check_doc_changes.py
index f673ad4ea..edc613f47 100755
--- a/scripts/dev/check_doc_changes.py
+++ b/scripts/dev/check_doc_changes.py
@@ -23,11 +23,17 @@
import sys
import subprocess
import os
+import os.path
-code = subprocess.run(['git', '--no-pager', 'diff',
- '--exit-code', '--stat'], check=False).returncode
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
+ os.pardir))
-if os.environ.get('TRAVIS_PULL_REQUEST', 'false') != 'false':
+from scripts import utils
+
+code = subprocess.run(['git', '--no-pager', 'diff', '--exit-code', '--stat',
+ '--', 'doc'], check=False).returncode
+
+if os.environ.get('GITHUB_REF', 'refs/heads/master') != 'refs/heads/master':
if code != 0:
print("Docs changed but ignoring change as we're building a PR")
sys.exit(0)
@@ -40,9 +46,9 @@ if code != 0:
print()
print('(Or you have uncommitted changes, in which case you can ignore '
'this.)')
- if 'TRAVIS' in os.environ:
+ if utils.ON_CI:
+ utils.gha_error('The autogenerated docs changed')
print()
- print("travis_fold:start:gitdiff")
- subprocess.run(['git', '--no-pager', 'diff'], check=True)
- print("travis_fold:end:gitdiff")
+ with utils.gha_group('Diff'):
+ subprocess.run(['git', '--no-pager', 'diff'], check=True)
sys.exit(code)
diff --git a/scripts/dev/ci/travis_backtrace.sh b/scripts/dev/ci/backtrace.sh
index 227dde8a8..f9b32f6d6 100644
--- a/scripts/dev/ci/travis_backtrace.sh
+++ b/scripts/dev/ci/backtrace.sh
@@ -4,13 +4,15 @@
# to determine exe using file(1) and dump stack trace with gdb.
#
-case $TESTENV in
+testenv=$1
+
+case $testenv in
py3*-pyqt*)
- exe=$(readlink -f ".tox/$TESTENV/bin/python")
+ exe=$(readlink -f ".tox/$testenv/bin/python")
full=
;;
*)
- echo "Skipping coredump analysis in testenv $TESTENV!"
+ echo "Skipping coredump analysis in testenv $testenv!"
exit 0
;;
esac
diff --git a/scripts/dev/ci/problemmatchers.py b/scripts/dev/ci/problemmatchers.py
new file mode 100644
index 000000000..320d0deeb
--- /dev/null
+++ b/scripts/dev/ci/problemmatchers.py
@@ -0,0 +1,214 @@
+#!/usr/bin/env python3
+# vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 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/>.
+
+"""Register problem matchers for GitHub Actions.
+
+Relevant docs:
+https://github.com/actions/toolkit/blob/master/docs/problem-matchers.md
+https://github.com/actions/toolkit/blob/master/docs/commands.md#problem-matchers
+"""
+
+import sys
+import pathlib
+import json
+
+
+MATCHERS = {
+ # scripts/dev/ci/run.sh:41:39: error: Double quote array expansions to
+ # avoid re-splitting elements. [SC2068]
+ "shellcheck": [
+ {
+ "pattern": [
+ {
+ "regexp": r"^(.+):(\d+):(\d+):\s(note|warning|error):\s(.*)\s\[(SC\d+)\]$",
+ "file": 1,
+ "line": 2,
+ "column": 3,
+ "severity": 4,
+ "message": 5,
+ "code": 6,
+ },
+ ],
+ },
+ ],
+
+ "yamllint": [
+ {
+ "pattern": [
+ {
+ "regexp": r"^\033\[4m([^\033]+)\033\[0m$",
+ "file": 1,
+ },
+ {
+ "regexp": r"^ \033\[2m(\d+):(\d+)\033\[0m \033\[3[13]m([^\033]+)\033\[0m +([^\033]*)\033\[2m\(([^)]+)\)\033\[0m$",
+ "line": 1,
+ "column": 2,
+ "severity": 3,
+ "message": 4,
+ "code": 5,
+ "loop": True,
+ },
+ ],
+ },
+ ],
+
+ # filename.py:313: unused function 'i_am_never_used' (60% confidence)
+ "vulture": [
+ {
+ "severity": "warning",
+ "pattern": [
+ {
+ "regexp": r"^([^:]+):(\d+): ([^(]+ \(\d+% confidence\))$",
+ "file": 1,
+ "line": 2,
+ "message": 3,
+ }
+ ]
+ },
+ ],
+
+ # filename.py:1:1: D100 Missing docstring in public module
+ "flake8": [
+ {
+ # "undefined name" is FXXX (i.e. not an error), but e.g. multiple
+ # spaces before an operator is EXXX (i.e. an error) - that makes little
+ # sense, so let's just treat everything as a warning instead.
+ "severity": "warning",
+ "pattern": [
+ {
+ "regexp": r"^(\033\[0m)?([^:]+):(\d+):(\d+): ([A-Z]\d{3}) (.*)$",
+ "file": 2,
+ "line": 3,
+ "column": 4,
+ "code": 5,
+ "message": 6,
+ },
+ ],
+ },
+ ],
+
+ # filename.py:80: error: Name 'foo' is not defined [name-defined]
+ "mypy": [
+ {
+ "pattern": [
+ {
+ "regexp": r"^(\033\[0m)?([^:]+):(\d+): ([^:]+): (.*) \[(.*)\]$",
+ "file": 2,
+ "line": 3,
+ "severity": 4,
+ "message": 5,
+ "code": 6,
+ },
+ ],
+ },
+ ],
+
+ # For some reason, ANSI color escape codes end up as part of the message
+ # GitHub gets with colored pylint output - so we have those escape codes
+ # (e.g. "\033[35m...\033[0m") as part of the regex patterns...
+ "pylint": [
+ {
+ # filename.py:80:10: E0602: Undefined variable 'foo' (undefined-variable)
+ "severity": "error",
+ "pattern": [
+ {
+ "regexp": r"^([^:]+):(\d+):(\d+): (E\d+): \033\[[\d;]+m([^\033]+).*$",
+ "file": 1,
+ "line": 2,
+ "column": 3,
+ "code": 4,
+ "message": 5,
+ },
+ ],
+ },
+ {
+ # filename.py:78:14: W0613: Unused argument 'unused' (unused-argument)
+ "severity": "warning",
+ "pattern": [
+ {
+ "regexp": r"^([^:]+):(\d+):(\d+): ([A-DF-Z]\d+): \033\[[\d;]+m([^\033]+).*$",
+ "file": 1,
+ "line": 2,
+ "column": 3,
+ "code": 4,
+ "message": 5,
+ },
+ ],
+ },
+ ],
+
+ "tests": [
+ {
+ # pytest test summary output
+ "severity": "error",
+ "pattern": [
+ {
+ "regexp": r'^=+ short test summary info =+$',
+ },
+ {
+ "regexp": r"^((ERROR|FAILED) .*)",
+ "message": 1,
+ "loop": True,
+ }
+ ],
+ },
+ {
+ # pytest error lines
+ # E end2end.fixtures.testprocess.WaitForTimeout: Timed out
+ # after 15000ms waiting for [...]
+ "severity": "error",
+ "pattern": [
+ {
+ "regexp": r'^\033\[1m\033\[31mE ([a-zA-Z0-9.]+: [^\033]*)\033\[0m$',
+ "message": 1,
+ },
+ ],
+ },
+ ],
+}
+
+
+def add_matcher(output_dir, owner, data):
+ data['owner'] = owner
+ out_data = {'problemMatcher': [data]}
+ output_file = output_dir / '{}.json'.format(owner)
+ with output_file.open('w', encoding='utf-8') as f:
+ json.dump(out_data, f)
+
+ print("::add-matcher::{}".format(output_file))
+
+
+def main(testenv, tempdir):
+ testenv = sys.argv[1]
+ if testenv.startswith('py3'):
+ testenv = 'tests'
+
+ if testenv not in MATCHERS:
+ return
+
+ output_dir = pathlib.Path(tempdir)
+
+ for idx, data in enumerate(MATCHERS[testenv]):
+ owner = '{}-{}'.format(testenv, idx)
+ add_matcher(output_dir=output_dir, owner=owner, data=data)
+
+
+if __name__ == '__main__':
+ sys.exit(main(*sys.argv[1:]))
diff --git a/scripts/dev/ci/travis_install.sh b/scripts/dev/ci/travis_install.sh
deleted file mode 100644
index 2975a52d7..000000000
--- a/scripts/dev/ci/travis_install.sh
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/bin/bash
-# vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et:
-
-# Copyright 2016-2017 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/>.
-
-# Stolen from https://github.com/travis-ci/travis-build/blob/master/lib/travis/build/templates/header.sh
-# and adjusted to use ((...))
-travis_retry() {
- local ANSI_RED='\033[31;1m'
- local ANSI_RESET='\033[0m'
- local result=0
- local count=1
- while (( count < 3 )); do
- if (( result != 0 )); then
- echo -e "\\n${ANSI_RED}The command \"$*\" failed. Retrying, $count of 3.${ANSI_RESET}\\n" >&2
- fi
- "$@"
- result=$?
- (( result == 0 )) && break
- count=$(( count + 1 ))
- sleep 1
- done
-
- if (( count > 3 )); then
- echo -e "\\n${ANSI_RED}The command \"$*\" failed 3 times.${ANSI_RESET}\\n" >&2
- fi
-
- return $result
-}
-
-pip_install() {
- travis_retry python3 -m pip install "$@"
-}
-
-npm_install() {
- # Make sure npm is up-to-date first
- travis_retry npm install -g npm
- travis_retry npm install -g "$@"
-}
-
-set -e
-
-if [[ -n $DOCKER ]]; then
- exit 0
-fi
-
-case $TESTENV in
- eslint)
- npm_install eslint
- ;;
- shellcheck)
- ;;
- *)
- pip_install -U pip
- pip_install -U -r misc/requirements/requirements-tox.txt
- if [[ $TESTENV == *-cov ]]; then
- pip_install -U -r misc/requirements/requirements-codecov.txt
- fi
- ;;
-esac
diff --git a/scripts/dev/ci/travis_run.sh b/scripts/dev/ci/travis_run.sh
deleted file mode 100644
index 96af14553..000000000
--- a/scripts/dev/ci/travis_run.sh
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/bin/bash
-
-if [[ -n $DOCKER ]]; then
- docker run \
- --privileged \
- -v "$PWD:/outside" \
- -e "QUTE_BDD_WEBENGINE=$QUTE_BDD_WEBENGINE" \
- -e "DOCKER=$DOCKER" \
- -e "CI=$CI" \
- -e "TRAVIS=$TRAVIS" \
- "qutebrowser/travis:$DOCKER"
-elif [[ $TESTENV == eslint ]]; then
- # Can't run this via tox as we can't easily install tox in the javascript
- # travis env
- cd qutebrowser/javascript || exit 1
- eslint --color --report-unused-disable-directives .
-elif [[ $TESTENV == shellcheck ]]; then
- SCRIPTS=$( mktemp )
- find scripts/dev/ -name '*.sh' >"$SCRIPTS"
- find misc/userscripts/ -type f -exec grep -lE '[/ ][bd]ash$|[/ ]sh$|[/ ]ksh$' {} + >>"$SCRIPTS"
- mapfile -t scripts <"$SCRIPTS"
- rm -f "$SCRIPTS"
- docker run \
- -v "$PWD:/outside" \
- -w /outside \
- koalaman/shellcheck:stable "${scripts[@]}"
-else
- args=()
- # We only run unit tests on macOS because it's quite slow.
- [[ $TRAVIS_OS_NAME == osx ]] && args+=('--qute-bdd-webengine' '--no-xvfb' 'tests/unit')
-
- # WORKAROUND for unknown crash inside swrast_dri.so
- # See https://github.com/qutebrowser/qutebrowser/pull/4218#issuecomment-421931770
- [[ $TESTENV == py36-pyqt59 ]] && export QT_QUICK_BACKEND=software
-
- tox -e "$TESTENV" -- "${args[@]}"
-fi
diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py
index 6bf411bba..366abc9ca 100644
--- a/scripts/dev/misc_checks.py
+++ b/scripts/dev/misc_checks.py
@@ -82,22 +82,20 @@ def check_git():
def check_spelling():
"""Check commonly misspelled words."""
# Words which I often misspell
- words = {'[Bb]ehaviour', '[Qq]uitted', 'Ll]ikelyhood', '[Ss]ucessfully',
- '[Oo]ccur[^rs .!]', '[Ss]eperator', '[Ee]xplicitely',
- '[Aa]uxillary', '[Aa]ccidentaly', '[Aa]mbigious', '[Ll]oosly',
- '[Ii]nitialis', '[Cc]onvienence', '[Ss]imiliar', '[Uu]ncommited',
- '[Rr]eproducable', '[Aa]n [Uu]ser', '[Cc]onvienience',
- '[Ww]ether', '[Pp]rogramatically', '[Ss]plitted', '[Ee]xitted',
- '[Mm]ininum', '[Rr]esett?ed', '[Rr]ecieved', '[Rr]egularily',
- '[Uu]nderlaying', '[Ii]nexistant', '[Ee]lipsis', 'commiting',
- 'existant', '[Rr]esetted', '[Ss]imilarily', '[Ii]nformations',
- '[Aa]n [Uu][Rr][Ll]', '[Tt]reshold'}
+ words = {'behaviour', 'quitted', 'likelyhood', 'sucessfully',
+ 'occur[^rs .!]', 'seperator', 'explicitely', 'auxillary',
+ 'accidentaly', 'ambigious', 'loosly', 'initialis', 'convienence',
+ 'similiar', 'uncommited', 'reproducable', 'an user',
+ 'convienience', 'wether', 'programatically', 'splitted',
+ 'exitted', 'mininum', 'resett?ed', 'recieved', 'regularily',
+ 'underlaying', 'inexistant', 'elipsis', 'commiting', 'existant',
+ 'resetted', 'similarily', 'informations', 'an url', 'treshold',
+ 'artefact'}
# Words which look better when splitted, but might need some fine tuning.
- words |= {'[Ww]ebelements', '[Mm]ouseevent', '[Kk]eysequence',
- '[Nn]ormalmode', '[Ee]ventloops', '[Ss]izehint',
- '[Ss]tatemachine', '[Mm]etaobject', '[Ll]ogrecord',
- '[Ff]iletype'}
+ words |= {'webelements', 'mouseevent', 'keysequence', 'normalmode',
+ 'eventloops', 'sizehint', 'statemachine', 'metaobject',
+ 'logrecord', 'filetype'}
# Files which should be ignored, e.g. because they come from another
# package
@@ -117,7 +115,8 @@ def check_spelling():
continue
for line in f:
for w in words:
- if (re.search(w, line) and
+ pattern = '[{}{}]{}'.format(w[0], w[0].upper(), w[1:])
+ if (re.search(pattern, line) and
fn not in seen[w] and
'# pragma: no spellcheck' not in line):
print('Found "{}" in {}!'.format(w, fn))
diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py
index 3e7db0c9a..7474c56c9 100644
--- a/scripts/dev/recompile_requirements.py
+++ b/scripts/dev/recompile_requirements.py
@@ -26,6 +26,7 @@ import os.path
import glob
import subprocess
import tempfile
+import argparse
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
os.pardir))
@@ -36,6 +37,84 @@ REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
'..', '..') # /scripts/dev -> /scripts -> /
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',
+ 'pytest-cov': 'https://github.com/pytest-dev/pytest-cov',
+ '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',
+ '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',
+ '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/',
+ 'attrs': 'http://www.attrs.org/en/stable/changelog.html',
+ 'jinja2': 'https://github.com/pallets/jinja/blob/master/CHANGES.rst',
+ 'flake8': 'https://gitlab.com/pycqa/flake8/tree/master/docs/source/release-notes',
+ 'cffi': 'https://cffi.readthedocs.io/en/latest/whatsnew.html',
+ 'flake8-debugger': 'https://github.com/JBKahn/flake8-debugger/',
+ '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',
+ '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',
+ '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',
+ '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',
+ 'typed-ast': 'https://github.com/python/typed_ast/commits/master',
+ '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.5/changes-4.5.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/commits/master',
+ 'cryptography': 'https://cryptography.io/en/latest/changelog/',
+ 'toml': 'https://github.com/uiri/toml/releases',
+ 'pyqt': 'https://www.riverbankcomputing.com/',
+ 'vulture': 'https://github.com/jendrikseipp/vulture/blob/master/CHANGELOG.md',
+ 'distlib': 'https://bitbucket.org/pypa/distlib/src/master/CHANGES.rst',
+ 'py-cpuinfo': 'https://github.com/workhorsy/py-cpuinfo/blob/master/ChangeLog',
+ 'cheroot': 'https://cheroot.cherrypy.org/en/latest/history.html',
+ 'certifi': 'https://ccadb-public.secure.force.com/mozilla/IncludedCACertificateReport',
+ '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',
+}
+
+# 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."""
@@ -121,74 +200,261 @@ 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)
+ utils.print_col('venv$ pip {}'.format(arg_str), 'blue')
+ venv_python = os.path.join(venv_dir, 'bin', 'python')
+ return subprocess.run([venv_python, '-m', 'pip'] + list(args),
+ check=True, **kwargs)
+
+
def init_venv(host_python, venv_dir, requirements, pre=False):
"""Initialize a new virtualenv and install the given packages."""
- subprocess.run([host_python, '-m', 'venv', venv_dir], check=True)
+ with utils.gha_group('Creating virtualenv'):
+ utils.print_col('$ python3 -m venv {}'.format(venv_dir), 'blue')
+ subprocess.run([host_python, '-m', 'venv', venv_dir], check=True)
- venv_python = os.path.join(venv_dir, 'bin', 'python')
- subprocess.run([venv_python, '-m', 'pip',
- 'install', '-U', 'pip'], check=True)
+ run_pip(venv_dir, 'install', '-U', 'pip')
+ run_pip(venv_dir, 'install', '-U', 'setuptools', 'wheel')
- install_command = [venv_python, '-m', 'pip', 'install', '-r', requirements]
+ install_command = ['install', '-r', requirements]
if pre:
install_command.append('--pre')
- subprocess.run(install_command, check=True)
- subprocess.run([venv_python, '-m', 'pip', 'check'], check=True)
- return venv_python
+ with utils.gha_group('Installing requirements'):
+ run_pip(venv_dir, *install_command)
+ run_pip(venv_dir, 'check')
-def main():
- """Re-compile the given (or all) requirement files."""
- names = sys.argv[1:] if len(sys.argv) > 1 else sorted(get_all_names())
- for name in names:
- utils.print_title(name)
- filename = os.path.join(REQ_DIR,
- 'requirements-{}.txt-raw'.format(name))
- if name == 'qutebrowser':
- outfile = os.path.join(REPO_DIR, 'requirements.txt')
+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()
+
+
+def git_diff(*args):
+ """Run a git diff command."""
+ command = (['git', '--no-pager', 'diff'] + list(args) + [
+ '--', 'requirements.txt', 'misc/requirements/requirements-*.txt'])
+ proc = subprocess.run(command,
+ stdout=subprocess.PIPE,
+ encoding='utf-8',
+ check=True)
+ return proc.stdout.splitlines()
+
+
+class Change:
+
+ """A single requirements change from a git diff output."""
+
+ def __init__(self, name):
+ self.name = name
+ self.old = None
+ self.new = None
+ if name.lower() in CHANGELOG_URLS:
+ self.url = CHANGELOG_URLS[name.lower()]
+ self.link = '[{}]({})'.format(self.name, self.url)
+ else:
+ self.url = '(no changelog)'
+ self.link = self.name
+
+ def __str__(self):
+ if self.old is None:
+ return '- {} new: {} {}'.format(self.name, self.new, self.url)
+ elif self.new is None:
+ return '- {} removed: {} {}'.format(self.name, self.old,
+ self.url)
else:
- outfile = os.path.join(REQ_DIR, 'requirements-{}.txt'.format(name))
-
- if name in [
- # Need sip v4 which doesn't work on Python 3.8
- 'pyqt-5.7', 'pyqt-5.9', 'pyqt-5.10', 'pyqt-5.11', 'pyqt-5.12',
- # Installs typed_ast on < 3.8 only
- 'pylint',
- ]:
- host_python = 'python3.7'
+ return '- {} {} -> {} {}'.format(self.name, self.old, self.new,
+ self.url)
+
+ def table_str(self):
+ """Generate a markdown table."""
+ if self.old is None:
+ return '| {} | -- | {} |'.format(self.link, self.new)
+ elif self.new is None:
+ return '| {} | {} | -- |'.format(self.link, self.old)
else:
- host_python = sys.executable
+ return '| {} | {} | {} |'.format(self.link, self.old, self.new)
+
+
+def print_changed_files():
+ """Output all changed files from this run."""
+ changed_files = set()
+ filenames = git_diff('--name-only')
+ for filename in filenames:
+ filename = filename.strip()
+ filename = filename.replace('misc/requirements/requirements-', '')
+ filename = filename.replace('.txt', '')
+ changed_files.add(filename)
+ files_text = '\n'.join('- ' + line for line in sorted(changed_files))
+
+ changes_dict = {}
+ diff = git_diff()
+ for line in diff:
+ if not line.startswith('-') and not line.startswith('+'):
+ continue
+ if line.startswith('+++ ') or line.startswith('--- '):
+ continue
+
+ if '==' in line:
+ name, version = line[1:].split('==')
+ else:
+ name = line[1:]
+ version = '?'
+
+ if name not in changes_dict:
+ changes_dict[name] = Change(name)
+
+ if line.startswith('-'):
+ changes_dict[name].old = version
+ elif line.startswith('+'):
+ changes_dict[name].new = version
+
+ changes = [change for _name, change in sorted(changes_dict.items())]
+ diff_text = '\n'.join(str(change) for change in changes)
+
+ utils.print_title('Changed')
+ utils.print_subtitle('Files')
+ print(files_text)
+ print()
+ utils.print_subtitle('Diff')
+ print(diff_text)
+
+ if 'CI' in os.environ:
+ print()
+ print('::set-output name=changed::' +
+ files_text.replace('\n', '%0A'))
+ table_header = [
+ '| Requirement | old | new |',
+ '|-------------|-----|-----|',
+ ]
+ diff_table = '%0A'.join(table_header +
+ [change.table_str() for change in changes])
+ print('::set-output name=diff::' + diff_table)
+
+
+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
+ """
+ if name in OLD_PYQT or name == 'pylint':
+ return 'python3.7'
+ else:
+ return sys.executable
+
+
+def build_requirements(name):
+ """Build a requirements file."""
+ utils.print_subtitle("Building")
+ filename = os.path.join(REQ_DIR, 'requirements-{}.txt-raw'.format(name))
+ host_python = get_host_python(name)
+
+ with open(filename, 'r', encoding='utf-8') as f:
+ comments = read_comments(f)
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ init_venv(host_python=host_python,
+ venv_dir=tmpdir,
+ requirements=filename,
+ pre=comments['pre'])
+ with utils.gha_group('Freezing requirements'):
+ proc = run_pip(tmpdir, 'freeze', stdout=subprocess.PIPE)
+ reqs = proc.stdout.decode('utf-8')
+ 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))
+
+ with open(outfile, 'w', encoding='utf-8') as f:
+ f.write("# This file is automatically generated by "
+ "scripts/dev/recompile_requirements.py\n\n")
+ for line in reqs.splitlines():
+ if line.startswith('qutebrowser=='):
+ continue
+ f.write(convert_line(line, comments) + '\n')
+
+ for line in comments['add']:
+ f.write(line + '\n')
+
+ return outfile
+
+
+def test_tox():
+ """Test requirements via tox."""
+ utils.print_title('Testing via tox')
+ host_python = get_host_python('tox')
+ req_path = os.path.join(REQ_DIR, 'requirements-tox.txt')
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ venv_dir = os.path.join(tmpdir, 'venv')
+ tox_workdir = os.path.join(tmpdir, 'tox-workdir')
+ venv_python = os.path.join(venv_dir, 'bin', 'python')
+ init_venv(host_python, venv_dir, req_path)
+ list_proc = subprocess.run([venv_python, '-m', 'tox', '--listenvs'],
+ check=True,
+ stdout=subprocess.PIPE,
+ universal_newlines=True)
+ environments = list_proc.stdout.strip().split('\n')
+ for env in environments:
+ with utils.gha_group('tox for {}'.format(env)):
+ utils.print_subtitle(env)
+ utils.print_col('venv$ tox -e {} --notest'.format(env), 'blue')
+ subprocess.run([venv_python, '-m', 'tox',
+ '--workdir', tox_workdir,
+ '-e', env,
+ '--notest'],
+ check=True)
+
+
+def test_requirements(name, outfile):
+ """Test a resulting requirements file."""
+ print()
+ utils.print_subtitle("Testing")
+
+ host_python = get_host_python(name)
+ with tempfile.TemporaryDirectory() as tmpdir:
+ init_venv(host_python, tmpdir, outfile)
- utils.print_subtitle("Building")
- with open(filename, 'r', encoding='utf-8') as f:
- comments = read_comments(f)
+def main():
+ """Re-compile the given (or all) requirement files."""
+ args = parse_args()
+ if args.names:
+ names = args.names
+ else:
+ names = filter_names(get_all_names(), old_pyqt=args.old_pyqt)
- with tempfile.TemporaryDirectory() as tmpdir:
- venv_python = init_venv(host_python=host_python,
- venv_dir=tmpdir,
- requirements=filename,
- pre=comments['pre'])
- proc = subprocess.run([venv_python, '-m', 'pip', 'freeze'],
- check=True, stdout=subprocess.PIPE)
- reqs = proc.stdout.decode('utf-8')
+ utils.print_col('Rebuilding requirements: ' + ', '.join(names), 'green')
+ for name in names:
+ utils.print_title(name)
+ outfile = build_requirements(name)
+ test_requirements(name, outfile)
+
+ if not args.names:
+ # If we selected a subset, let's not go through the trouble of testing
+ # via tox.
+ test_tox()
- with open(outfile, 'w', encoding='utf-8') as f:
- f.write("# This file is automatically generated by "
- "scripts/dev/recompile_requirements.py\n\n")
- for line in reqs.splitlines():
- if line.startswith('qutebrowser=='):
- continue
- f.write(convert_line(line, comments) + '\n')
-
- for line in comments['add']:
- f.write(line + '\n')
-
- # Test resulting file
- utils.print_subtitle("Testing")
- with tempfile.TemporaryDirectory() as tmpdir:
- init_venv(host_python, tmpdir, outfile)
+ print_changed_files()
if __name__ == '__main__':
diff --git a/scripts/dev/run_shellcheck.sh b/scripts/dev/run_shellcheck.sh
new file mode 100644
index 000000000..885e68375
--- /dev/null
+++ b/scripts/dev/run_shellcheck.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 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/>.
+
+set -e
+
+script_list=$(mktemp)
+find scripts/dev/ -name '*.sh' > "$script_list"
+find misc/userscripts/ -type f -exec grep -lE '[/ ][bd]ash$|[/ ]sh$|[/ ]ksh$' {} + >> "$script_list"
+mapfile -t scripts < "$script_list"
+rm -f "$script_list"
+
+if [[ $1 == --docker ]]; then
+ shift 1
+ docker run \
+ -v "$PWD:/outside" \
+ -w /outside \
+ -t \
+ koalaman/shellcheck:stable "$@" "${scripts[@]}"
+else
+ shellcheck --version
+ shellcheck "$@" "${scripts[@]}"
+fi
diff --git a/scripts/dev/update_version.py b/scripts/dev/update_version.py
index 13bd3d776..e86ff257d 100644
--- a/scripts/dev/update_version.py
+++ b/scripts/dev/update_version.py
@@ -88,9 +88,3 @@ if __name__ == "__main__":
print("* macOS: git fetch && git checkout v{v} && "
"python3 scripts/dev/build_release.py --upload"
.format(v=version))
-
- print("* On server:")
- print(" - bash download_release.sh {v}"
- .format(v=version))
- print(" - git pull github master && sudo python3 "
- "scripts/asciidoc2html.py --website /srv/http/qutebrowser")
diff --git a/scripts/utils.py b/scripts/utils.py
index bdf3f96fc..f46e6a4de 100644
--- a/scripts/utils.py
+++ b/scripts/utils.py
@@ -22,6 +22,7 @@
import os
import os.path
import sys
+import contextlib
# Import side-effects are an evil thing, but here it's okay so scripts using
@@ -54,6 +55,9 @@ fg_colors = {
bg_colors = {name: col + 10 for name, col in fg_colors.items()}
+ON_CI = 'CI' in os.environ
+
+
def _esc(code):
"""Get an ANSI color code based on a color number."""
return '\033[{}m'.format(code)
@@ -64,9 +68,9 @@ def print_col(text, color, file=sys.stdout):
if use_color:
fg = _esc(fg_colors[color.lower()])
reset = _esc(fg_colors['reset'])
- print(''.join([fg, text, reset]), file=file)
+ print(''.join([fg, text, reset]), file=file, flush=True)
else:
- print(text, file=file)
+ print(text, file=file, flush=True)
def print_error(text):
@@ -90,3 +94,26 @@ def change_cwd():
cwd = os.getcwd()
if os.path.split(cwd)[1] == 'scripts':
os.chdir(os.path.join(cwd, os.pardir))
+
+
+@contextlib.contextmanager
+def gha_group(name):
+ """Print a GitHub Actions group.
+
+ Gets ignored if not on CI.
+ """
+ if ON_CI:
+ print('::group::' + name)
+ yield
+ print('::endgroup::')
+ else:
+ yield
+
+
+def gha_error(message):
+ """Print a GitHub Actions error.
+
+ Should only be called on CI.
+ """
+ assert ON_CI
+ print('::error::' + message)