diff --git a/.travis.yml b/.travis.yml index 0f1c6a4a42f9..6d651b39928b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,65 @@ language: c++ -matrix: + +# Build flags common to OS X and Linux. +# Parallel builds are important for avoiding OSX build timeouts. +# We turn off verbose output to avoid going over the 4MB output limit. +# TODO(2019-07-21): Add "ffmpeg=1" if FFmpeg 4.x becomes available in Ubuntu +env: + global: + - COMMON_FLAGS="-j4 test=1 mad=1 faad=1 opus=1 modplug=1 wv=1 hss1394=0 virtualize=0 debug_assertions_fatal=1 verbose=0" + +jobs: include: - - os: linux + - name: qsscheck + os: linux + dist: xenial + before_install: + - pip3 install tinycss + script: + - ./scripts/qsscheck.py . + addons: + apt: + packages: + - python3 + - python3-pip + - python3-pyqt5 + - python3-setuptools + - python3-wheel + + - name: Ubuntu/gcc build + os: linux dist: xenial - sudo: required compiler: gcc - - os: osx + # Ubuntu Xenial build prerequisites + env: EXTRA_FLAGS="localecompare=1" + install: + - scons $COMMON_FLAGS $EXTRA_FLAGS + script: + # NOTE(sblaisot): 2018-01-02 removing gdb wrapper on linux due to a bug in + # return code in order to avoid having a successful build when a test fail. + # https://bugs.launchpad.net/mixxx/+bug/1699689 + - ./mixxx-test + + - name: OSX/clang build + os: osx compiler: clang + # Workaround for bug in libopus's opus.h including + # instead of . + # Virtual X (Xvfb) is needed for analyzer waveform tests + env: >- + CFLAGS="-isystem /usr/local/include/opus" + CXXFLAGS="-isystem /usr/local/include/opus" + DISPLAY=:99.0 + before_install: + - export QTDIR="$(find /usr/local/Cellar/qt -d 1 | tail -n 1)" + - echo "QTDIR=$QTDIR" + install: + - scons $COMMON_FLAGS $EXTRA_FLAGS + script: + # lldb doesn't provide an easy way to exit 1 on error: + # https://bugs.llvm.org/show_bug.cgi?id=27326 + - lldb ./mixxx-test --batch -o run -o quit -k 'thread backtrace all' -k "script import os; os._exit(1)" git: depth: 1 @@ -82,44 +134,6 @@ addons: - taglib - wavpack -install: - # Build flags common to OS X and Linux. - # Parallel builds are important for avoiding OSX build timeouts. - # We turn off verbose output to avoid going over the 4MB output limit. - # TODO(2019-07-21): Add "ffmpeg=1" if FFmpeg 4.x becomes available in Ubuntu - - export COMMON_FLAGS="-j4 test=1 mad=1 faad=1 opus=1 modplug=1 wv=1 hss1394=0 virtualize=0 debug_assertions_fatal=1 verbose=0" - - # Ubuntu Xenial build prerequisites - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then export EXTRA_FLAGS="localecompare=1"; fi - - # Define QTDIR. - - if [ "$TRAVIS_OS_NAME" = "osx" ]; then export QTDIR=$(find /usr/local/Cellar/qt -d 1 | tail -n 1); fi - - # Workaround for bug in libopus's opus.h including - # instead of . - - if [ "$TRAVIS_OS_NAME" = "osx" ]; then export CXXFLAGS="-isystem /usr/local/include/opus"; fi - - if [ "$TRAVIS_OS_NAME" = "osx" ]; then export CFLAGS="-isystem /usr/local/include/opus"; fi - - # NOTE(rryan): 2016-11-15 we are experiencing Travis timeouts for the OSX - # build. Turning off optimizations to see if that speeds up compile times. - # TODO(rryan): localecompare doesn't work on Travis with qt5 for some reason. - # TODO(2019-07-21): Move "ffmpeg=1" into COMMON_FLAGS if FFmpeg 4.x becomes available in Ubuntu - - if [ "$TRAVIS_OS_NAME" = "osx" ]; then export EXTRA_FLAGS="ffmpeg=1 optimize=none asan=0 localecompare=0"; fi - - - scons $COMMON_FLAGS $EXTRA_FLAGS - -before_script: - # Virtual X (Xvfb) is needed for analyzer waveform tests - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then export DISPLAY=:99.0; fi - -script: - # NOTE(sblaisot): 2018-01-02 removing gdb wrapper on linux due to a bug in - # return code in order to avoid having a successful build when a test fail. - # https://bugs.launchpad.net/mixxx/+bug/1699689 - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then ./mixxx-test; fi - # lldb doesn't provide an easy way to exit 1 on error: - # https://bugs.llvm.org/show_bug.cgi?id=27326 - - if [ "$TRAVIS_OS_NAME" = "osx" ]; then lldb ./mixxx-test --batch -o run -o quit -k 'thread backtrace all' -k "script import os; os._exit(1)"; fi notifications: webhooks: diff --git a/scripts/qsscheck.py b/scripts/qsscheck.py new file mode 100755 index 000000000000..c0d331815216 --- /dev/null +++ b/scripts/qsscheck.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import argparse +import fnmatch +import os +import os.path +import re +import sys +import tinycss.css21 +import PyQt5.QtWidgets + + +RE_CPP_CLASSNAME = re.compile(r'^\s*class\s+([\w_]+)') +RE_CPP_OBJNAME = re.compile(r'setObjectName\(.*"([^"]+)"') +RE_UI_OBJNAME = re.compile(r']+name="([^"]+)"') +RE_XML_OBJNAME = re.compile(r'(.*)') +RE_XML_OBJNAME_SETVAR = re.compile( + r'(.*)') +RE_CLASSNAME = re.compile(r'^[A-Z]\w+$') +RE_OBJNAME_VARTAG = re.compile(r'<.*>') + + +def get_skins(path): + """Yields (skin_name, skin_path) tuples for each skin directory in path.""" + for entry in os.scandir(path): + if entry.is_dir(): + yield entry.name, os.path.join(path, entry.name) + + +def get_global_names(mixxx_path): + """Returns 2 sets with all class and object names in the Mixx codebase.""" + classnames = set() + objectnames = set() + for root, dirs, fnames in os.walk(os.path.join(mixxx_path, 'src')): + for fname in fnames: + ext = os.path.splitext(fname)[1] + if ext in ('.h', '.cpp'): + fpath = os.path.join(root, fname) + with open(fpath, mode='r') as f: + for line in f: + classnames.update(set(RE_CPP_CLASSNAME.findall(line))) + objectnames.update(set(RE_CPP_OBJNAME.findall(line))) + elif ext == '.ui': + fpath = os.path.join(root, fname) + with open(fpath, mode='r') as f: + objectnames.update(set(RE_UI_OBJNAME.findall(f.read()))) + return classnames, objectnames + + +def get_skin_objectnames(skin_path): + """ + Yields all object names in the skin_path. + + Note the names may contain one or more tags, so it's + not enough to check if a name CSS object name is in this list using "in". + """ + for root, dirs, fnames in os.walk(skin_path): + for fname in fnames: + if os.path.splitext(fname)[1] != '.xml': + continue + + fpath = os.path.join(root, fname) + with open(fpath, mode='r') as f: + for line in f: + yield from RE_XML_OBJNAME.findall(line) + yield from RE_XML_OBJNAME_SETVAR.findall(line) + + +def get_skin_stylesheets(skin_path): + """Yields (qss_path, stylesheet) tuples for each qss file in skin_path).""" + cssparser = tinycss.css21.CSS21Parser() + for filename in os.listdir(skin_path): + if os.path.splitext(filename)[1] != '.qss': + continue + qss_path = os.path.join(skin_path, filename) + stylesheet = cssparser.parse_stylesheet_file(qss_path) + yield qss_path, stylesheet + + +def check_stylesheet(stylesheet, classnames, objectnames, objectnames_fuzzy): + """Yields (token, problem) tuples for each problem found in stylesheet.""" + for rule in stylesheet.rules: + if not isinstance(rule, tinycss.css21.RuleSet): + continue + for token in rule.selector: + if token.type == 'IDENT': + if not RE_CLASSNAME.match(token.value): + continue + if token.value in classnames: + continue + if token.value in dir(PyQt5.QtWidgets): + continue + yield (token, 'Unknown widget class "%s"' % token.value) + + elif token.type == 'HASH': + value = token.value[1:] + if value in objectnames: + continue + + if any(fnmatch.fnmatchcase(value, objname) + for objname in objectnames_fuzzy): + continue + + yield (token, 'Unknown object name "%s"' % token.value) + + +def check_skins(mixxx_path, skins, ignore_patterns=()): + """ + Yields error messages for skins using class/object names from mixxx_path. + + By providing a list of ignore_patterns, you can ignore certain class or + object names (e.g. #Test, #*Debug). + """ + classnames, objectnames = get_global_names(mixxx_path) + for skin_name, skin_path in sorted(skins): + # If the skin objectname is something like 'Deck', + # then replace it with 'Deck*' and use glob-like matching + skin_objectnames = objectnames.copy() + skin_objectnames_fuzzy = set() + for objname in get_skin_objectnames(skin_path): + new_objname = RE_OBJNAME_VARTAG.sub('*', objname) + if '*' in new_objname: + skin_objectnames_fuzzy.add(new_objname) + else: + skin_objectnames.add(new_objname) + + for qss_path, stylesheet in get_skin_stylesheets(skin_path): + for error in stylesheet.errors: + yield '%s:%d:%d: %s - %s' % ( + qss_path, error.line, error.column, + error.__class__.__name__, error.reason, + ) + for token, message in check_stylesheet( + stylesheet, classnames, + skin_objectnames, skin_objectnames_fuzzy): + if any(fnmatch.fnmatchcase(token.value, pattern) + for pattern in ignore_patterns): + continue + yield '%s:%d:%d: %s' % ( + qss_path, token.line, token.column, message, + ) + + +def main(argv=None): + """Main method for handling command line arguments.""" + parser = argparse.ArgumentParser('qsscheck', description='Check Mixxx QSS ' + 'stylesheets for non-existing ' + 'object/class names') + parser.add_argument('-p', '--extra-skins-path', + help='Additonal skin path, to check (.e.g. ' + '"~/.mixxx/skins")') + parser.add_argument('-s', '--skin', help='Only check skin with this name') + parser.add_argument('-i', '--ignore', default='', + help='Glob pattern of class/object names to ignore ' + '(e.g. "#Test*"), separated by commas') + parser.add_argument('mixxx_path', help='Path of Mixxx sources/git repo') + args = parser.parse_args(argv) + + mixxx_path = args.mixxx_path + + skins_path = os.path.join(mixxx_path, 'res', 'skins') + skins = set(get_skins(skins_path)) + if args.extra_skins_path: + skins.update(set(get_skins(args.extra_skins_path))) + + if args.skin: + skins = set((name, path) for name, path in skins if name == args.skin) + + if not skins: + print('No skins to check') + return 1 + + status = 0 + for message in check_skins(mixxx_path, skins, args.ignore.split(',')): + print(message) + status = 2 + return status + + +if __name__ == '__main__': + sys.exit(main())