From 2b53fa22e4343b37b8f458cdc5182350d8d629bb Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Wed, 16 Feb 2022 14:43:22 +0900 Subject: [PATCH] Imported Upstream version 1.10 --- .dir-locals.el | 10 - .dockerignore | 2 + .editorconfig | 12 ++ .travis.yml | 36 ++-- AbstractCheck.py | 12 +- AppDataCheck.py | 4 +- BinariesCheck.py | 143 ++++++------- Config.py | 11 +- ConfigCheck.py | 3 +- DistributionCheck.py | 6 +- DocFilesCheck.py | 2 - FHSCheck.py | 3 +- FilesCheck.py | 224 ++++++++++++--------- Filter.py | 18 +- I18NCheck.py | 4 +- InitScriptCheck.py | 16 +- LSBCheck.py | 3 +- Makefile | 6 +- MenuCheck.py | 30 ++- MenuXDGCheck.py | 5 +- NamingPolicyCheck.py | 7 +- PamCheck.py | 7 +- Pkg.py | 45 +++-- PostCheck.py | 23 +-- RpmFileCheck.py | 5 +- SCLCheck.py | 2 +- SignatureCheck.py | 7 +- SourceCheck.py | 5 +- SpecCheck.py | 119 ++++++----- TagsCheck.py | 32 ++- ZipCheck.py | 10 +- config | 5 + rpmdiff | 12 +- rpmdiff.1 | 4 +- rpmlint | 29 +-- rpmlint.bash-completion | 2 +- setup.cfg | 2 +- test.sh | 18 +- test/Dockerfile-centos6 | 18 ++ test/Dockerfile-fedora26 | 23 +++ test/Dockerfile-fedoradev | 26 +++ test/Dockerfile-ubuntu14 | 18 ++ .../netmask-debugsource-2.4.3-5.fc27.x86_64.rpm | Bin 0 -> 19038 bytes test/docker-script.sh | 11 + test/spec/SpecCheck.spec | 6 + test/test.PamCheck.py | 1 + test/test.Pkg.py | 7 + test/test.SpecCheck.py | 9 +- test/test.SpecCheck2.py | 1 + test/test.SpecCheck3.py | 1 + test/test.config | 2 + test/test_binaries.py | 17 +- test/test_distribution.py | 17 ++ test/test_files.py | 30 ++- test/test_scl.py | 51 ++--- tools/Testing.py | 23 ++- 56 files changed, 685 insertions(+), 460 deletions(-) delete mode 100644 .dir-locals.el create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 test/Dockerfile-centos6 create mode 100644 test/Dockerfile-fedora26 create mode 100644 test/Dockerfile-fedoradev create mode 100644 test/Dockerfile-ubuntu14 create mode 100644 test/binary/netmask-debugsource-2.4.3-5.fc27.x86_64.rpm create mode 100755 test/docker-script.sh create mode 100644 test/test_distribution.py diff --git a/.dir-locals.el b/.dir-locals.el deleted file mode 100644 index e088633..0000000 --- a/.dir-locals.el +++ /dev/null @@ -1,10 +0,0 @@ -((python-mode . ( - (indent-tabs-mode . nil) - (python-indent-offset . 4) - )) - (shell-script-mode . ( - (indent-tabs-mode . nil) - (sh-basic-offset . 4) - (sh-indent-comment . t) - )) -) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..34fcef1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +*.pyo +*.pyc diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..74e2655 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +[*] +indent_style = space +indent_size = 4 +tab_width = 8 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 79 + +[Makefile] +indent_style = tab diff --git a/.travis.yml b/.travis.yml index 925a612..e2bda8d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,30 +1,18 @@ -language: python +language: generic -# python-rpm is not available through pip, so we get it with apt-get -# and configure the virtualenv to use system site packages. As long as -# that's the case, we need to stick with the system installed Python -# versions only. Unfortunately as python3-rpm is not available even -# for Trusty, that means 2.7 only for now. +sudo: required -dist: trusty +services: + - docker -python: - - "2.7" +env: + - DIST=centos6 PYTHON=python PYTEST=py.test FLAKE8=true + #- DIST=ubuntu14 PYTHON=python PYTEST=py.test FLAKE8=true + - DIST=fedora26 PYTHON=python3 PYTEST=py.test-3 FLAKE8=flake8-3 + - DIST=fedoradev PYTHON=python3 PYTEST=py.test FLAKE8=flake8 -virtualenv: - system_site_packages: true - -addons: - apt: - packages: - - python-enchant - - python-magic - -install: - # https://github.com/travis-ci/apt-package-whitelist/issues/1696 - - sudo apt-get update -qq - - sudo apt-get install -qq python-rpm rpm rpm2cpio - - pip install pytest flake8 flake8-import-order hacking +before_install: + - docker build -t $DIST -f test/Dockerfile-$DIST . script: - - make check + - docker run -e PYTHON=$PYTHON -e PYTEST=$PYTEST -e FLAKE8=$FLAKE8 $DIST test/docker-script.sh diff --git a/AbstractCheck.py b/AbstractCheck.py index 27dbc5c..34a4586 100644 --- a/AbstractCheck.py +++ b/AbstractCheck.py @@ -7,6 +7,7 @@ # Purpose : Abstract class to hold all the derived classes. ############################################################################# +import contextlib import re try: import urllib2 @@ -17,7 +18,7 @@ import Config from Filter import addDetails, printInfo, printWarning # Note: do not add any capturing parentheses here -macro_regex = re.compile('%+[{(]?[a-zA-Z_]\w{2,}[)}]?') +macro_regex = re.compile(r'%+[{(]?[a-zA-Z_]\w{2,}[)}]?') class _HeadRequest(urllib2.Request): @@ -55,7 +56,7 @@ class AbstractCheck(object): def check_binary(self, pkg): return - def check_spec(self, pkg, spec_file): + def check_spec(self, pkg, spec_file, spec_lines=None): return def check_url(self, pkg, tag, url): @@ -83,8 +84,8 @@ class AbstractCheck(object): printWarning(pkg, 'invalid-url', '%s:' % tag, url, errstr) info = None if res: - info = res.info() - res.close() + with contextlib.closing(res): + info = res.info() return info @@ -105,6 +106,7 @@ class AbstractFilesCheck(AbstractCheck): """ raise NotImplementedError('check must be implemented in subclass') + addDetails( 'invalid-url', '''The value should be a valid, public HTTP, HTTPS, or FTP URL.''', @@ -115,5 +117,3 @@ see the NetworkEnabled option.''', ) # AbstractCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/AppDataCheck.py b/AppDataCheck.py index 1271d09..43320a6 100644 --- a/AppDataCheck.py +++ b/AppDataCheck.py @@ -23,7 +23,7 @@ class AppDataCheck(AbstractCheck.AbstractFilesCheck): # $ echo $XDG_DATA_DIRS/applications # /var/lib/menu-xdg:/usr/share AbstractCheck.AbstractFilesCheck.__init__( - self, "AppDataCheck", "/usr/share/appdata/.*\.appdata.xml$") + self, "AppDataCheck", r"/usr/share/appdata/.*\.appdata.xml$") def check_file(self, pkg, filename): root = pkg.dirName() @@ -43,5 +43,3 @@ addDetails( 'invalid-appdata-file', '''appdata file is not valid, check with %s''' % (" ".join(appdata_checker)), ) - -# ex: ts=4 sw=4 et diff --git a/BinariesCheck.py b/BinariesCheck.py index 33dfae5..887e0f0 100644 --- a/BinariesCheck.py +++ b/BinariesCheck.py @@ -10,6 +10,7 @@ import os import re import stat +import subprocess import rpm @@ -25,34 +26,36 @@ DEFAULT_SYSTEM_LIB_PATHS = ( def create_regexp_call(call): - r = ".*?\s+(%s(?:@GLIBC\S+)?)(?:\s|$)" % call + r = r".*?\s+(%s(?:@GLIBC\S+)?)(?:\s|$)" % call return re.compile(r) def create_nonlibc_regexp_call(call): - r = ".*?\s+UND\s+(%s)\s?.*$" % call + r = r".*?\s+UND\s+(%s)\s?.*$" % call return re.compile(r) class BinaryInfo(object): - needed_regex = re.compile('\s+\(NEEDED\).*\[(\S+)\]') - rpath_regex = re.compile('\s+\(RPATH\).*\[(\S+)\]') - soname_regex = re.compile('\s+\(SONAME\).*\[(\S+)\]') - comment_regex = re.compile('^\s+\[\s*\d+\]\s+\.comment\s+') - pic_regex = re.compile('^\s+\[\s*\d+\]\s+\.rela?\.(data|text)') + needed_regex = re.compile(r'\s+\(NEEDED\).*\[(\S+)\]') + rpath_regex = re.compile(r'\s+\(RPATH\).*\[(\S+)\]') + soname_regex = re.compile(r'\s+\(SONAME\).*\[(\S+)\]') + comment_regex = re.compile(r'^\s+\[\s*\d+\]\s+\.comment\s+') + pic_regex = re.compile(r'^\s+\[\s*\d+\]\s+\.rela?\.(data|text)') # GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4 - stack_regex = re.compile('^\s+GNU_STACK\s+(?:(?:\S+\s+){5}(\S+)\s+)?') - stack_exec_regex = re.compile('^..E$') - undef_regex = re.compile('^undefined symbol:\s+(\S+)') - unused_regex = re.compile('^\s+(\S+)') - call_regex = re.compile('\s0\s+FUNC\s+(.*)') - exit_call_regex = create_regexp_call('_?exit') - fork_call_regex = create_regexp_call('fork') - setgid_call_regex = create_regexp_call('set(?:res|e)?gid') - setuid_call_regex = create_regexp_call('set(?:res|e)?uid') - setgroups_call_regex = create_regexp_call('(?:ini|se)tgroups') + stack_regex = re.compile(r'^\s+GNU_STACK\s+(?:(?:\S+\s+){5}(\S+)\s+)?') + stack_exec_regex = re.compile(r'^..E$') + undef_regex = re.compile(r'^undefined symbol:\s+(\S+)') + unused_regex = re.compile(r'^\s+(\S+)') + call_regex = re.compile(r'\s0\s+FUNC\s+(.*)') + exit_call_regex = create_regexp_call(r'_?exit') + fork_call_regex = create_regexp_call(r'fork') + setgid_call_regex = create_regexp_call(r'set(?:res|e)?gid') + setuid_call_regex = create_regexp_call(r'set(?:res|e)?uid') + setgroups_call_regex = create_regexp_call(r'(?:ini|se)tgroups') chroot_call_regex = create_regexp_call('chroot') + # 401eb8: e8 c3 f0 ff ff callq 400f80 + objdump_call_regex = re.compile(br'callq?\s(.*)') forbidden_functions = Config.getOption("WarnOnFunction") if forbidden_functions: @@ -94,9 +97,8 @@ class BinaryInfo(object): is_debug = path.endswith('.debug') - cmd = ['env', 'LC_ALL=C', 'readelf', '-W', '-S', '-l', '-d', '-s'] - cmd.append(path) - res = Pkg.getstatusoutput(cmd) + res = Pkg.getstatusoutput( + ('readelf', '-W', '-S', '-l', '-d', '-s', path)) if not res[0]: lines = res[1].splitlines() for l in lines: @@ -178,9 +180,7 @@ class BinaryInfo(object): # check if we don't have a string that will automatically # waive the presence of a forbidden call if self.forbidden_calls: - cmd = ['env', 'LC_ALL=C', 'strings'] - cmd.append(path) - res = Pkg.getstatusoutput(cmd) + res = Pkg.getstatusoutput(('strings', path)) if not res[0]: for l in res[1].splitlines(): # as we need to remove elements, iterate backwards @@ -204,28 +204,37 @@ class BinaryInfo(object): # check if chroot is near chdir (since otherwise, chroot is called # without chdir) - if self.chroot and self.chdir: - # FIXME this check is too slow, because forking for objdump is - # quite slow according to a quick test and that's quite visible - # on a server like postfix - res = Pkg.getstatusoutput( - ('env', 'LC_ALL=C', 'objdump', '-d', path)) - if res[0]: + # Currently this implementation works only on x86_64 due to reliance + # on x86_64 specific assembly. Skip it on other architectures + if pkg.arch == 'x86_64' and self.chroot and self.chdir: + p = subprocess.Popen(('objdump', '-d', path), + stdout=subprocess.PIPE, bufsize=-1, + env=dict(os.environ, LC_ALL="C")) + with p.stdout: + index = 0 + chroot_index = -99 + chdir_index = -99 + for line in p.stdout: + res = BinaryInfo.objdump_call_regex.search(line) + if not res: + continue + if b'@plt' not in res.group(1): + pass + elif b'chroot@plt' in res.group(1): + chroot_index = index + if abs(chroot_index - chdir_index) <= 2: + self.chroot_near_chdir = True + break + elif b'chdir@plt' in res.group(1): + chdir_index = index + if abs(chroot_index - chdir_index) <= 2: + self.chroot_near_chdir = True + break + index += 1 + if p.wait() and not self.chroot_near_chdir: printWarning(pkg, 'binaryinfo-objdump-failed', file) self.chroot_near_chdir = True # avoid false positive - else: - call = [] - # we want that : - # 401eb8: e8 c3 f0 ff ff callq 400f80 - for l in res[1].splitlines(): - # call is for x86 32 bits, callq for x86_64 - if l.find('callq ') >= 0 or l.find('call ') >= 0: - call.append(l.rpartition(' ')[2]) - for index, c in enumerate(call): - if c.find('chroot@plt') >= 0: - for i in call[index - 2:index + 2]: - if i.find('chdir@plt'): - self.chroot_near_chdir = True + else: self.readelf_error = True printWarning(pkg, 'binaryinfo-readelf-failed', @@ -243,26 +252,22 @@ class BinaryInfo(object): # skip debuginfo: https://bugzilla.redhat.com/190599 if not is_ar and not is_debug and isinstance(pkg, Pkg.InstalledPkg): # We could do this with objdump, but it's _much_ simpler with ldd. - res = Pkg.getstatusoutput( - ('env', 'LC_ALL=C', 'ldd', '-d', '-r', path)) + res = Pkg.getstatusoutput(('ldd', '-d', '-r', path)) if not res[0]: for l in res[1].splitlines(): undef = BinaryInfo.undef_regex.search(l) if undef: self.undef.append(undef.group(1)) if self.undef: - cmd = self.undef[:] - cmd.insert(0, 'c++filt') try: - res = Pkg.getstatusoutput(cmd) + res = Pkg.getstatusoutput(['c++filt'] + self.undef) if not res[0]: self.undef = res[1].splitlines() except: pass else: printWarning(pkg, 'ldd-failed', file) - res = Pkg.getstatusoutput( - ('env', 'LC_ALL=C', 'ldd', '-r', '-u', path)) + res = Pkg.getstatusoutput(('ldd', '-r', '-u', path)) if res[0]: # Either ldd doesn't grok -u (added in glibc 2.3.4) or we have # unused direct dependencies @@ -279,25 +284,26 @@ class BinaryInfo(object): else: in_unused = False -path_regex = re.compile('(.*/)([^/]+)') -numeric_dir_regex = re.compile('/usr(?:/share)/man/man./(.*)\.[0-9](?:\.gz|\.bz2)') -versioned_dir_regex = re.compile('[^.][0-9]') -ldso_soname_regex = re.compile('^ld(-linux(-(ia|x86_)64))?\.so') -so_regex = re.compile('/lib(64)?/[^/]+\.so(\.[0-9]+)*$') -validso_regex = re.compile('(\.so\.\d+(\.\d+)*|\d\.so)$') -sparc_regex = re.compile('SPARC32PLUS|SPARC V9|UltraSPARC') + +path_regex = re.compile(r'(.*/)([^/]+)') +numeric_dir_regex = re.compile(r'/usr(?:/share)/man/man./(.*)\.[0-9](?:\.gz|\.bz2)') +versioned_dir_regex = re.compile(r'[^.][0-9]') +ldso_soname_regex = re.compile(r'^ld(-linux(-(ia|x86_)64))?\.so') +so_regex = re.compile(r'/lib(64)?/[^/]+\.so(\.[0-9]+)*$') +validso_regex = re.compile(r'(\.so\.\d+(\.\d+)*|\d\.so)$') +sparc_regex = re.compile(r'SPARC32PLUS|SPARC V9|UltraSPARC') system_lib_paths = Config.getOption('SystemLibPaths', DEFAULT_SYSTEM_LIB_PATHS) pie_exec_re = Config.getOption('PieExecutables') if pie_exec_re: pie_exec_re = re.compile(pie_exec_re) -usr_lib_regex = re.compile('^/usr/lib(64)?/') -bin_regex = re.compile('^(/usr(/X11R6)?)?/s?bin/') -soversion_regex = re.compile('.*?([0-9][.0-9]*)\\.so|.*\\.so\\.([0-9][.0-9]*).*') -reference_regex = re.compile('\.la$|^/usr/lib(64)?/pkgconfig/') -usr_lib_exception_regex = re.compile(Config.getOption('UsrLibBinaryException', '^/usr/lib(64)?/(perl|python|ruby|menu|pkgconfig|ocaml|lib[^/]+\.(so|l?a)$|bonobo/servers/)')) -srcname_regex = re.compile('(.*?)-[0-9]') -invalid_dir_ref_regex = re.compile('/(home|tmp)(\W|$)') -ocaml_mixed_regex = re.compile('^Caml1999X0\d\d$') +usr_lib_regex = re.compile(r'^/usr/lib(64)?/') +bin_regex = re.compile(r'^(/usr(/X11R6)?)?/s?bin/') +soversion_regex = re.compile(r'.*?([0-9][.0-9]*)\\.so|.*\\.so\\.([0-9][.0-9]*).*') +reference_regex = re.compile(r'\.la$|^/usr/lib(64)?/pkgconfig/') +usr_lib_exception_regex = re.compile(Config.getOption('UsrLibBinaryException', r'^/usr/lib(64)?/(perl|python|ruby|menu|pkgconfig|ocaml|lib[^/]+\.(so|l?a)$|bonobo/servers/|\.build-id)')) +srcname_regex = re.compile(r'(.*?)-[0-9]') +invalid_dir_ref_regex = re.compile(r'/(home|tmp)(\W|$)') +ocaml_mixed_regex = re.compile(r'^Caml1999X0\d\d$') def dir_base(path): @@ -525,7 +531,7 @@ class BinariesCheck(AbstractCheck.AbstractCheck): printError(pkg, 'missing-call-to-setgroups-before-setuid', fname) - if bin_info.chroot: + if pkg.arch == 'x86_64' and bin_info.chroot: if not bin_info.chdir or not bin_info.chroot_near_chdir: printError(pkg, 'missing-call-to-chdir-with-chroot', fname) @@ -550,6 +556,7 @@ class BinariesCheck(AbstractCheck.AbstractCheck): if has_usr_lib_file and not binary_in_usr_lib: printWarning(pkg, 'only-non-binary-in-usr-lib') + # Create an object to enable the auto registration of the test check = BinariesCheck() @@ -589,6 +596,8 @@ run at package postinstall phase.)''', '''The listed shared libraries contain object code that was compiled without -fPIC. All object code in shared libraries should be recompiled separately from the static libraries with the -fPIC option. +Use the ``eu-findtextrel'' command on a library with debugging symbols +to list code compiled without -fPIC. Another common mistake that causes this problem is linking with ``gcc -Wl,-shared'' instead of ``gcc -shared''.''', @@ -718,5 +727,3 @@ implementations only strip if the permission is 0755).''' ) # BinariesCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/Config.py b/Config.py index a742932..6ed0a32 100644 --- a/Config.py +++ b/Config.py @@ -58,7 +58,7 @@ _checks.extend(DEFAULT_CHECKS) def addCheck(check): - check = re.sub('\.py[co]?$', '', check) + check = re.sub(r'\.py[co]?$', '', check) if check not in _checks: _checks.append(check) @@ -79,6 +79,7 @@ def resetChecks(): _checks = [] + # handle the list of directories to look for checks _dirs = ["/usr/share/rpmlint"] @@ -93,6 +94,7 @@ def addCheckDir(dir): def checkDirs(): return _dirs + # handle options _options = {} @@ -108,6 +110,7 @@ def getOption(name, default=""): except: return default + # List of filters _filters = [] _filters_re = None @@ -130,6 +133,7 @@ def removeFilter(s): else: _filters_re = None + _scoring = {} @@ -140,7 +144,8 @@ def setBadness(s, score): def badness(s): return _scoring.get(s, 0) -_non_named_group_re = re.compile('[^\\](\()[^:]') + +_non_named_group_re = re.compile(r'[^\\](\()[^:]') def isFiltered(s): @@ -167,5 +172,3 @@ def isFiltered(s): return False # Config.py ends here - -# ex: ts=4 sw=4 et diff --git a/ConfigCheck.py b/ConfigCheck.py index 35b69f3..1db9573 100644 --- a/ConfigCheck.py +++ b/ConfigCheck.py @@ -29,6 +29,7 @@ class ConfigCheck(AbstractCheck.AbstractCheck): if c not in noreplace_files: printWarning(pkg, "conffile-without-noreplace-flag", c) + # Create an object to enable the auto registration of the test check = ConfigCheck() @@ -52,5 +53,3 @@ A way to resolve this is to put the following in your SPEC file: ) # ConfigCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/DistributionCheck.py b/DistributionCheck.py index 6f85e06..e2e6c63 100644 --- a/DistributionCheck.py +++ b/DistributionCheck.py @@ -16,8 +16,8 @@ import Config from Filter import addDetails, printWarning -man_regex = re.compile("/man(?:\d[px]?|n)/") -info_regex = re.compile("(/usr/share|/usr)/info/") +man_regex = re.compile(r"/man(?:\d[px]?|n)/") +info_regex = re.compile(r"(/usr/share|/usr)/info/") vendor = Config.getOption("Vendor") distribution = Config.getOption("Distribution") compress_ext = Config.getOption("CompressExtension", "bz2") @@ -79,5 +79,3 @@ you can compress this file in the %%install section of the spec file.''' ) # DistributionCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/DocFilesCheck.py b/DocFilesCheck.py index a633ccf..958c184 100644 --- a/DocFilesCheck.py +++ b/DocFilesCheck.py @@ -100,5 +100,3 @@ installed, do not include the file in the binary package.''', ) # DocFilesCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/FHSCheck.py b/FHSCheck.py index 3a8dd03..75ae1c5 100644 --- a/FHSCheck.py +++ b/FHSCheck.py @@ -49,6 +49,7 @@ class FHSCheck(AbstractCheck.AbstractCheck): printWarning(pkg, "non-standard-dir-in-var", d) var_list.append(d) + # Create an object to enable the auto registration of the test check = FHSCheck() @@ -70,5 +71,3 @@ directories are: ) # FHSCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/FilesCheck.py b/FilesCheck.py index a53d2da..a7724de 100644 --- a/FilesCheck.py +++ b/FilesCheck.py @@ -178,58 +178,60 @@ DEFAULT_DISALLOWED_DIRS = ( '/var/tmp', ) -sub_bin_regex = re.compile('^(/usr)?/s?bin/\S+/') -backup_regex = re.compile('(~|\#[^/]+\#|\.orig|\.rej)$') -compr_regex = re.compile('\.(gz|z|Z|zip|bz2|lzma|xz)$') -absolute_regex = re.compile('^/([^/]+)') -absolute2_regex = re.compile('^/?([^/]+)') -points_regex = re.compile('^\.\./(.*)') -doc_regex = re.compile('^/usr(/share|/X11R6)?/(doc|man|info)/') -bin_regex = re.compile('^/(?:usr/(?:s?bin|games)|s?bin)/(.*)') -includefile_regex = re.compile('\.(c|h)(pp|xx)?$', re.IGNORECASE) -develfile_regex = re.compile('\.(a|cmxa?|mli?|gir)$') -buildconfigfile_regex = re.compile('(\.pc|/bin/.+-config)$') +sub_bin_regex = re.compile(r'^(/usr)?/s?bin/\S+/') +backup_regex = re.compile(r'(~|\#[^/]+\#|\.orig|\.rej)$') +compr_regex = re.compile(r'\.(gz|z|Z|zip|bz2|lzma|xz)$') +absolute_regex = re.compile(r'^/([^/]+)') +absolute2_regex = re.compile(r'^/?([^/]+)') +points_regex = re.compile(r'^\.\./(.*)') +doc_regex = re.compile(r'^/usr(/share|/X11R6)?/(doc|man|info)/') +bin_regex = re.compile(r'^/(?:usr/(?:s?bin|games)|s?bin)/(.*)') +includefile_regex = re.compile(r'\.(c|h)(pp|xx)?$', re.IGNORECASE) +develfile_regex = re.compile(r'\.(a|cmxa?|mli?|gir)$') +buildconfigfile_regex = re.compile(r'(\.pc|/bin/.+-config)$') # room for improvement with catching more -R, but also for false positives... -buildconfig_rpath_regex = re.compile('(?:-rpath|Wl,-R)\\b') -sofile_regex = re.compile('/lib(64)?/(.+/)?lib[^/]+\.so$') -devel_regex = re.compile('(.*)-(debug(info)?|devel|headers|source|static)$') -debuginfo_package_regex = re.compile('-debug(info)?$') -lib_regex = re.compile('lib(64)?/lib[^/]*(\.so\..*|-[0-9.]+\.so)') -ldconfig_regex = re.compile('^[^#]*ldconfig', re.MULTILINE) -depmod_regex = re.compile('^[^#]*depmod', re.MULTILINE) -install_info_regex = re.compile('^[^#]*install-info', re.MULTILINE) -perl_temp_file_regex = re.compile('.*perl.*/(\.packlist|perllocal\.pod)$') -scm_regex = re.compile('/CVS/[^/]+$|/\.(bzr|cvs|git|hg)ignore$|/\.hgtags$|/\.(bzr|git|hg|svn)/|/(\.arch-ids|{arch})/') -games_path_regex = re.compile('^/usr(/lib(64)?)?/games/') +buildconfig_rpath_regex = re.compile(r'(?:-rpath|Wl,-R)\b') +sofile_regex = re.compile(r'/lib(64)?/(.+/)?lib[^/]+\.so$') +devel_regex = re.compile(r'(.*)-(debug(info|source)?|devel|headers|source|static)$') +debuginfo_package_regex = re.compile(r'-debug(info)?$') +debugsource_package_regex = re.compile(r'-debugsource$') +use_debugsource = Config.getOption('UseDebugSource', False) +lib_regex = re.compile(r'lib(64)?/lib[^/]*(\.so\..*|-[0-9.]+\.so)') +ldconfig_regex = re.compile(r'^[^#]*ldconfig', re.MULTILINE) +depmod_regex = re.compile(r'^[^#]*depmod', re.MULTILINE) +install_info_regex = re.compile(r'^[^#]*install-info', re.MULTILINE) +perl_temp_file_regex = re.compile(r'.*perl.*/(\.packlist|perllocal\.pod)$') +scm_regex = re.compile(r'/CVS/[^/]+$|/\.(bzr|cvs|git|hg)ignore$|/\.hgtags$|/\.(bzr|git|hg|svn)/|/(\.arch-ids|{arch})/') +games_path_regex = re.compile(r'^/usr(/lib(64)?)?/games/') games_group_regex = re.compile(Config.getOption('RpmGamesGroups', DEFAULT_GAMES_GROUPS)) dangling_exceptions = Config.getOption('DanglingSymlinkExceptions', DEFAULT_DANGLING_EXCEPTIONS) -logrotate_regex = re.compile('^/etc/logrotate\.d/(.*)') +logrotate_regex = re.compile(r'^/etc/logrotate\.d/(.*)') module_rpms_ok = Config.getOption('KernelModuleRPMsOK', True) -kernel_modules_regex = re.compile('^(?:/usr)/lib/modules/([0-9]+\.[0-9]+\.[0-9]+[^/]*?)/') -kernel_package_regex = re.compile('^kernel(22)?(-)?(smp|enterprise|bigmem|secure|BOOT|i686-up-4GB|p3-smp-64GB)?') -normal_zero_length_regex = re.compile('^/etc/security/console\.apps/|/\.nosearch$|/__init__\.py$') -perl_regex = re.compile('^/usr/lib/perl5/(?:vendor_perl/)?([0-9]+\.[0-9]+)\.([0-9]+)/') -python_regex = re.compile('^/usr/lib(?:64)?/python([.0-9]+)/') -python_bytecode_regex_pep3147 = re.compile('^(.*)/__pycache__/(.*?)\.([^.]+)(\.opt-[12])?\.py[oc]$') -python_bytecode_regex = re.compile('^(.*)(\.py[oc])$') +kernel_modules_regex = re.compile(r'^(?:/usr)/lib/modules/([0-9]+\.[0-9]+\.[0-9]+[^/]*?)/') +kernel_package_regex = re.compile(r'^kernel(22)?(-)?(smp|enterprise|bigmem|secure|BOOT|i686-up-4GB|p3-smp-64GB)?') +normal_zero_length_regex = re.compile(r'^/etc/security/console\.apps/|/\.nosearch$|/__init__\.py$') +perl_regex = re.compile(r'^/usr/lib/perl5/(?:vendor_perl/)?([0-9]+\.[0-9]+)\.([0-9]+)/') +python_regex = re.compile(r'^/usr/lib(?:64)?/python([.0-9]+)/') +python_bytecode_regex_pep3147 = re.compile(r'^(.*)/__pycache__/(.*?)\.([^.]+)(\.opt-[12])?\.py[oc]$') +python_bytecode_regex = re.compile(r'^(.*)(\.py[oc])$') python_default_version = Config.getOption('PythonDefaultVersion', None) perl_version_trick = Config.getOption('PerlVersionTrick', True) -log_regex = re.compile('^/var/log/[^/]+$') -lib_path_regex = re.compile('^(/usr(/X11R6)?)?/lib(64)?') -lib_package_regex = re.compile('^(lib|.+-libs)') -hidden_file_regex = re.compile('/\.[^/]*$') -manifest_perl_regex = re.compile('^/usr/share/doc/perl-.*/MANIFEST(\.SKIP)?$') -shebang_regex = re.compile(b'^#!\s*(\S+)(.*?)$', re.M) -interpreter_regex = re.compile('^/(?:usr/)?(?:s?bin|games|libexec(?:/.+)?|(?:lib(?:64)?|share)/.+)/([^/]+)$') -script_regex = re.compile('^/((usr/)?s?bin|etc/(rc\.d/init\.d|X11/xinit\.d|cron\.(hourly|daily|monthly|weekly)))/') -sourced_script_regex = re.compile('^/etc/(bash_completion\.d|profile\.d)/') +log_regex = re.compile(r'^/var/log/[^/]+$') +lib_path_regex = re.compile(r'^(/usr(/X11R6)?)?/lib(64)?') +lib_package_regex = re.compile(r'^(lib|.+-libs)') +hidden_file_regex = re.compile(r'/\.[^/]*$') +manifest_perl_regex = re.compile(r'^/usr/share/doc/perl-.*/MANIFEST(\.SKIP)?$') +shebang_regex = re.compile(br'^#!\s*(\S+)(.*?)$', re.M) +interpreter_regex = re.compile(r'^/(?:usr/)?(?:s?bin|games|libexec(?:/.+)?|(?:lib(?:64)?|share)/.+)/([^/]+)$') +script_regex = re.compile(r'^/((usr/)?s?bin|etc/(rc\.d/init\.d|X11/xinit\.d|cron\.(hourly|daily|monthly|weekly)))/') +sourced_script_regex = re.compile(r'^/etc/(bash_completion\.d|profile\.d)/') use_utf8 = Config.getOption('UseUTF8', Config.USEUTF8_DEFAULT) -skipdocs_regex = re.compile(Config.getOption('SkipDocsRegexp', '\.(?:rtf|x?html?|svg|ml[ily]?)$'), re.IGNORECASE) -meta_package_regex = re.compile(Config.getOption('MetaPackageRegexp', '^(bundle|task)-')) +skipdocs_regex = re.compile(Config.getOption('SkipDocsRegexp', r'\.(?:rtf|x?html?|svg|ml[ily]?)$'), re.IGNORECASE) +meta_package_regex = re.compile(Config.getOption('MetaPackageRegexp', r'^(bundle|task)-')) filesys_packages = ['filesystem'] # TODO: make configurable? -quotes_regex = re.compile('[\'"]+') -start_certificate_regex = re.compile('^-----BEGIN CERTIFICATE-----$') -start_private_key_regex = re.compile('^----BEGIN PRIVATE KEY-----$') +quotes_regex = re.compile(r'[\'"]+') +start_certificate_regex = re.compile(r'^-----BEGIN CERTIFICATE-----$') +start_private_key_regex = re.compile(r'^----BEGIN PRIVATE KEY-----$') for idx in range(0, len(dangling_exceptions)): dangling_exceptions[idx][0] = re.compile(dangling_exceptions[idx][0]) @@ -242,8 +244,8 @@ standard_users = Config.getOption('StandardUsers', DEFAULT_STANDARD_USERS) disallowed_dirs = Config.getOption('DisallowedDirs', DEFAULT_DISALLOWED_DIRS) -non_readable_regexs = (re.compile('^/var/log/'), - re.compile('^/etc/(g?shadow-?|securetty)$')) +non_readable_regexs = (re.compile(r'^/var/log/'), + re.compile(r'^/etc/(g?shadow-?|securetty)$')) man_base_regex = re.compile(r'^/usr(?:/share)?/man(?:/overrides)?/man[^/]+/(.+)\.[1-9n]') man_warn_regex = re.compile(r'^([^:]+:)\d+:\s*') @@ -256,8 +258,8 @@ man_nowarn_regex = re.compile( r'(can\'t break|cannot adjust) line') man_warn_category = Config.getOption('ManWarningCategory', 'mac') -fsf_license_regex = re.compile(b'(GNU((\s+(Library|Lesser|Affero))?(\s+General)?\s+Public|\s+Free\s+Documentation)\s+Licen[cs]e|(GP|FD)L)', re.IGNORECASE) -fsf_wrong_address_regex = re.compile(b'(675\s+Mass\s+Ave|59\s+Temple\s+Place|Franklin\s+Steet|02139|02111-1307)', re.IGNORECASE) +fsf_license_regex = re.compile(br'(GNU((\s+(Library|Lesser|Affero))?(\s+General)?\s+Public|\s+Free\s+Documentation)\s+Licen[cs]e|(GP|FD)L)', re.IGNORECASE) +fsf_wrong_address_regex = re.compile(br'(675\s+Mass\s+Ave|59\s+Temple\s+Place|Franklin\s+Steet|02139|02111-1307)', re.IGNORECASE) scalable_icon_regex = re.compile(r'^/usr(?:/local)?/share/icons/.*/scalable/') @@ -308,23 +310,26 @@ def peek(filename, pkg, length=1024): return (chunk, istext) + # See Python sources for a full list of the values here. -# http://hg.python.org/cpython/file/tip/Lib/importlib/_bootstrap_external.py -# http://hg.python.org/cpython/file/2.7/Python/import.c +# https://github.com/python/cpython/blob/master/Lib/importlib/_bootstrap_external.py +# https://github.com/python/cpython/blob/2.7/Python/import.c +# https://github.com/python/cpython/commit/93602e3af70d3b9f98ae2da654b16b3382b68d50 _python_magic_values = { - '2.2': 60717, - '2.3': 62011, - '2.4': 62061, - '2.5': 62131, - '2.6': 62161, - '2.7': 62211, - '3.0': 3130, - '3.1': 3150, - '3.2': 3180, - '3.3': 3230, - '3.4': 3310, - '3.5': 3350, - '3.6': 3361, + '2.2': [60717], + '2.3': [62011], + '2.4': [62061], + '2.5': [62131], + '2.6': [62161], + '2.7': [62211], + '3.0': [3130], + '3.1': [3150], + '3.2': [3180], + '3.3': [3230], + '3.4': [3310], + '3.5': [3350, 3351], # 3350 for < 3.5.2 + '3.6': [3379], + '3.7': [3390], } @@ -332,11 +337,11 @@ def get_expected_pyc_magic(path): """ .pyc/.pyo files embed a 4-byte magic value identifying which version of the python bytecode ABI they are for. Given a path to a .pyc/.pyo file, - return a (magic ABI value, python version) tuple. For example, + return a (magic ABI values, python version) tuple. For example, '/usr/lib/python3.1/foo.pyc' should return (3151, '3.1'). The first value will be None if the python version was not resolved from the given pathname and the PythonDefaultVersion configuration - variable is not set, or if we don't know the magic ABI value for the + variable is not set, or if we don't know the magic ABI values for the python version (no matter from which source the version came from). The second value will be None if a python version could not be resolved from the given pathname. @@ -348,18 +353,18 @@ def get_expected_pyc_magic(path): ver_from_path = m.group(1) expected_version = ver_from_path or python_default_version - expected_magic_value = _python_magic_values.get(expected_version) + expected_magic_values = _python_magic_values.get(expected_version) - if not expected_magic_value: + if not expected_magic_values: return (None, ver_from_path) # In Python 2, if Py_UnicodeFlag is set, Python's import code uses a value # one higher, but this is off by default. In Python 3.0 and 3.1 (but no # longer in 3.2), it always uses the value one higher: if expected_version[:3] in ('3.0', '3.1'): - expected_magic_value += 1 + expected_magic_values = [x + 1 for x in expected_magic_values] - return (expected_magic_value, ver_from_path) + return (expected_magic_values, ver_from_path) def py_demarshal_long(b): @@ -423,6 +428,7 @@ class FilesCheck(AbstractCheck.AbstractCheck): lib_package = lib_package_regex.search(pkg.name) is_kernel_package = kernel_package_regex.search(pkg.name) debuginfo_package = debuginfo_package_regex.search(pkg.name) + debugsource_package = debugsource_package_regex.search(pkg.name) # report these errors only once perl_dep_error = False @@ -440,7 +446,7 @@ class FilesCheck(AbstractCheck.AbstractCheck): if files: if meta_package_regex.search(pkg.name): printWarning(pkg, 'file-in-meta-package') - elif debuginfo_package: + elif debuginfo_package or debugsource_package: printError(pkg, 'empty-debuginfo-package') # Prefetch scriptlets, strip quotes from them (#169) @@ -494,6 +500,12 @@ class FilesCheck(AbstractCheck.AbstractCheck): if f.startswith('/run/'): if f not in ghost_files: printWarning(pkg, 'non-ghost-in-run', f) + elif f.startswith('/etc/systemd/system/'): + printWarning(pkg, 'systemd-unit-in-etc', f) + elif f.startswith('/etc/udev/rules.d/'): + printWarning(pkg, 'udev-rule-in-etc', f) + elif f.startswith('/etc/tmpfiles.d/'): + printWarning(pkg, 'tmpfiles-conf-in-etc', f) elif sub_bin_regex.search(f): printError(pkg, 'subdir-in-bin', f) elif '/site_perl/' in f: @@ -505,7 +517,7 @@ class FilesCheck(AbstractCheck.AbstractCheck): printError(pkg, 'version-control-internal-file', f) elif f.endswith('/.htaccess'): printError(pkg, 'htaccess-file', f) - elif hidden_file_regex.search(f) and not f.startswith("/etc/skel/"): + elif hidden_file_regex.search(f) and not f.startswith("/etc/skel/") and not f.endswith("/.build-id"): printWarning(pkg, 'hidden-file-or-dir', f) elif manifest_perl_regex.search(f): printWarning(pkg, 'manifest-in-perl-module', f) @@ -521,7 +533,7 @@ class FilesCheck(AbstractCheck.AbstractCheck): if link != '': ext = compr_regex.search(link) if ext: - if not re.compile('\.' + ext.group(1) + '$').search(f): + if not re.compile(r'\.%s$' % ext.group(1)).search(f): printError(pkg, 'compressed-symlink-with-wrong-ext', f, link) @@ -572,8 +584,14 @@ class FilesCheck(AbstractCheck.AbstractCheck): chunk = None istext = False - if os.access(pkgfile.path, os.R_OK): - (chunk, istext) = peek(pkgfile.path, pkg) + res = None + try: + res = os.access(pkgfile.path, os.R_OK) + except UnicodeError as e: # e.g. non-ASCII, C locale, python 3 + printWarning(pkg, 'inaccessible-filename', f, e) + else: + if res: + (chunk, istext) = peek(pkgfile.path, pkg) (interpreter, interpreter_args) = script_interpreter(chunk) @@ -607,9 +625,9 @@ class FilesCheck(AbstractCheck.AbstractCheck): if res: kernel_version = res.group(1) kernel_version_regex = re.compile( - '\\bdepmod\s+-a.*F\s+/boot/System\.map-' + - re.escape(kernel_version) + '\\b.*\\b' + - re.escape(kernel_version) + '\\b', + r'\bdepmod\s+-a.*F\s+/boot/System\.map-' + + re.escape(kernel_version) + r'\b.*\b' + + re.escape(kernel_version) + r'\b', re.MULTILINE | re.DOTALL) if not postin or not depmod_regex.search(postin): @@ -713,18 +731,18 @@ class FilesCheck(AbstractCheck.AbstractCheck): # .pyc header is correct found_magic = py_demarshal_long(chunk[:4]) & 0xffff exp_magic, exp_version = get_expected_pyc_magic(f) - if exp_magic and found_magic != exp_magic: + if exp_magic and found_magic not in exp_magic: found_version = 'unknown' for (pv, pm) in _python_magic_values.items(): - if pm == found_magic: + if found_magic in pm: found_version = pv break # If expected version was from the file path, # issue # an error, otherwise a warning. msg = (pkg, 'python-bytecode-wrong-magic-value', - f, "expected %d (%s), found %d (%s)" % - (exp_magic, + f, "expected %s (%s), found %d (%s)" % + (" or ".join(map(str, exp_magic)), exp_version or python_default_version, found_magic, found_version)) if exp_version is not None: @@ -788,12 +806,11 @@ class FilesCheck(AbstractCheck.AbstractCheck): if use_utf8 and chunk: # TODO: sequence based invocation cmd = getstatusoutput( - 'env LC_ALL=C %s %s | gtbl | ' - 'env LC_ALL=en_US.UTF-8 groff -mtty-char -Tutf8 ' - '-P-c -mandoc -w%s >/dev/null' % + '%s %s | gtbl | groff -mtty-char -Tutf8 ' + '-P-c -mandoc -w%s >%s' % (catcmd(f), shquote(pkgfile.path), - shquote(man_warn_category)), - shell=True) + shquote(man_warn_category), os.devnull), + shell=True, lc_all="en_US.UTF-8") for line in cmd[1].split("\n"): res = man_warn_regex.search(line) if not res or man_nowarn_regex.search(line): @@ -805,12 +822,11 @@ class FilesCheck(AbstractCheck.AbstractCheck): and scalable_icon_regex.search(f): printWarning(pkg, "gzipped-svg-icon", f) - if f.endswith('.pem'): - if f not in ghost_files: - if pkg.grep(start_certificate_regex, f): - printWarning(pkg, 'pem-certificate', f) - if pkg.grep(start_private_key_regex, f): - printError(pkg, 'pem-private-key', f) + if f.endswith('.pem') and f not in ghost_files: + if pkg.grep(start_certificate_regex, f): + printWarning(pkg, 'pem-certificate', f) + if pkg.grep(start_private_key_regex, f): + printError(pkg, 'pem-private-key', f) # text file checks if istext: @@ -873,7 +889,7 @@ class FilesCheck(AbstractCheck.AbstractCheck): printError(pkg, 'non-standard-dir-perm', f, "%o" % perm) if pkg.name not in filesys_packages and f in STANDARD_DIRS: printError(pkg, 'standard-dir-owned-by-package', f) - if hidden_file_regex.search(f): + if hidden_file_regex.search(f) and not f.endswith("/.build-id"): printWarning(pkg, 'hidden-file-or-dir', f) # symbolic link check @@ -990,7 +1006,7 @@ class FilesCheck(AbstractCheck.AbstractCheck): if lib_package and lib_file and non_lib_file: printError(pkg, 'outside-libdir-files', non_lib_file) - if debuginfo_package and debuginfo_debugs and not debuginfo_srcs: + if not use_debugsource and debuginfo_package and debuginfo_debugs and not debuginfo_srcs: printError(pkg, 'debuginfo-without-sources') for exe, paths in bindir_exes.items(): @@ -999,6 +1015,7 @@ class FilesCheck(AbstractCheck.AbstractCheck): if exe not in man_basenames: printWarning(pkg, "no-manual-page-for-binary", exe) + # Create an object to enable the auto registration of the test check = FilesCheck() @@ -1060,6 +1077,18 @@ file is beginning with a dot (.) and contain "perl" in its name.''', in this directory should be marked as %ghost and created at runtime to work properly in tmpfs /run setups.''', +'systemd-unit-in-etc', +'''A systemd unit has been packaged in /etc/systemd/system. These units should +be installed in the system unit dir instead.''', + +'udev-rule-in-etc', +'''A udev rule has been packaged in /etc/udev/rules.d. These rules should be +installed in the system rules dir instead.''', + +'tmpfiles-conf-in-etc', +'''A tmpfiles config has been packaged in /etc/tmpfiles.d. These rules should be +installed in the system tmpfiles dir instead.''', + 'subdir-in-bin', '''The package contains a subdirectory in /usr/bin. It's not permitted to create a subdir there. Create it in /usr/lib/ instead.''', @@ -1304,6 +1333,11 @@ it in the rpm header indicates that it is supposed to be a readable normal file but it actually is not in the filesystem. Because of this, some checks will be skipped.''', +'inaccessible-filename', +'''An error occurred while trying to access this file due to some characters +in its name. Because of this, some checks will be skipped. Access could work +with some other locale settings.''', + 'executable-crontab-file', '''This crontab file has executable bit set, which is refused by newer version of cron''', @@ -1375,5 +1409,3 @@ for packages to install files in this directory.''' % i) # FilesCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/Filter.py b/Filter.py index 0f56ecc..a728115 100644 --- a/Filter.py +++ b/Filter.py @@ -9,6 +9,7 @@ from __future__ import print_function +import codecs import locale import sys import textwrap @@ -25,17 +26,20 @@ _diagnostic = list() _badness_score = 0 printed_messages = {"I": 0, "W": 0, "E": 0} -__stdout = sys.stdout __preferred_encoding = locale.getpreferredencoding() if sys.version_info[0] < 3: - import codecs __stdout = codecs.getwriter(__preferred_encoding)(sys.stdout, 'replace') + def __print(s): + if isinstance(s, str): + s = s.decode(__preferred_encoding, 'replace') + print(s, file=__stdout) +else: + __stdout = codecs.getwriter(__preferred_encoding)( + sys.stdout.buffer, 'replace') -def __print(s): - if isinstance(s, str) and hasattr(s, 'decode'): - s = s.decode(__preferred_encoding, 'replace') - print(s, file=__stdout) + def __print(s): + print(s, file=__stdout) def printInfo(pkg, reason, *details): @@ -157,5 +161,3 @@ def setRawOut(file): _rawout = open(file, "w") # Filter.py ends here - -# ex: ts=4 sw=4 et diff --git a/I18NCheck.py b/I18NCheck.py index da9b1f8..ac2c300 100644 --- a/I18NCheck.py +++ b/I18NCheck.py @@ -60,7 +60,6 @@ def is_valid_lang(lang): return False # TODO: don't accept all lang_COUNTRY combinations - country = lang[ix + 1:] if country not in COUNTRIES: return False @@ -157,6 +156,7 @@ class I18NCheck(AbstractCheck.AbstractCheck): def is_prefix(p, s): return len(p) <= len(s) and p == s[:len(p)] + # Create an object to enable the auto registration of the test check = I18NCheck() @@ -197,5 +197,3 @@ installable if XX is not in %_install_langs.""", ) # I18NCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/InitScriptCheck.py b/InitScriptCheck.py index ee7d52e..9df2b9a 100644 --- a/InitScriptCheck.py +++ b/InitScriptCheck.py @@ -19,14 +19,14 @@ from Filter import addDetails, printError, printWarning import Pkg -chkconfig_content_regex = re.compile('^\s*#\s*chkconfig:\s*([-0-9]+)\s+[-0-9]+\s+[-0-9]+') -subsys_regex = re.compile('/var/lock/subsys/([^/"\'\n\s;&|]+)', re.MULTILINE) -chkconfig_regex = re.compile('^[^#]*(chkconfig|add-service|del-service)', re.MULTILINE) -status_regex = re.compile('^[^#]*status', re.MULTILINE) -reload_regex = re.compile('^[^#]*reload', re.MULTILINE) +chkconfig_content_regex = re.compile(r'^\s*#\s*chkconfig:\s*([-0-9]+)\s+[-0-9]+\s+[-0-9]+') +subsys_regex = re.compile(r'/var/lock/subsys/([^/"\'\s;&|]+)', re.MULTILINE) +chkconfig_regex = re.compile(r'^[^#]*(chkconfig|add-service|del-service)', re.MULTILINE) +status_regex = re.compile(r'^[^#]*status', re.MULTILINE) +reload_regex = re.compile(r'^[^#]*reload', re.MULTILINE) use_deflevels = Config.getOption('UseDefaultRunlevels', True) -lsb_tags_regex = re.compile('^# ([\w-]+):\s*(.*?)\s*$') -lsb_cont_regex = re.compile('^#(?:\t| )(.*?)\s*$') +lsb_tags_regex = re.compile(r'^# ([\w-]+):\s*(.*?)\s*$') +lsb_cont_regex = re.compile(r'^#(?:%s| )(.*?)\s*$' % "\t") use_subsys = Config.getOption('UseVarLockSubsys', True) LSB_KEYWORDS = ('Provides', 'Required-Start', 'Required-Stop', 'Should-Start', @@ -275,5 +275,3 @@ in order for it to run at boot time.''', ) # InitScriptCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/LSBCheck.py b/LSBCheck.py index 743f2a3..e29bb8b 100644 --- a/LSBCheck.py +++ b/LSBCheck.py @@ -39,6 +39,7 @@ class LSBCheck(AbstractCheck.AbstractCheck): if release and not version_regex.search(release): printError(pkg, 'non-lsb-compliant-release', release) + # Create an object to enable the auto registration of the test check = LSBCheck() @@ -58,5 +59,3 @@ lowercase letters and/or numbers.""", ) # LSBCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/Makefile b/Makefile index 56a8e1f..1eb2c0a 100644 --- a/Makefile +++ b/Makefile @@ -20,11 +20,7 @@ PACKAGE = rpmlint PYTHON = /usr/bin/python # update this variable to create a new release -VERSION := 1.9 - -# for the [A-Z]* part -LC_ALL:=C -export LC_ALL +VERSION := 1.10 all: __version__.py __isocodes__.py diff --git a/MenuCheck.py b/MenuCheck.py index 4d426d5..004565d 100644 --- a/MenuCheck.py +++ b/MenuCheck.py @@ -134,24 +134,24 @@ DEFAULT_LAUNCHERS = ( ['(?:/usr/bin/)?soundwrapper', None], ) -menu_file_regex = re.compile('^/usr/lib/menu/([^/]+)$') -old_menu_file_regex = re.compile('^/usr/share/(gnome/apps|applnk)/([^/]+)$') -package_regex = re.compile('\?package\((.*)\):') -needs_regex = re.compile('needs=(\"([^\"]+)\"|([^ \t\"]+))') -section_regex = re.compile('section=(\"([^\"]+)\"|([^ \t\"]+))') -title_regex = re.compile('[\"\s]title=(\"([^\"]+)\"|([^ \t\"]+))') -longtitle_regex = re.compile('longtitle=(\"([^\"]+)\"|([^ \t\"]+))') -command_regex = re.compile('command=(?:\"([^\"]+)\"|([^ \t\"]+))') -icon_regex = re.compile('icon=\"?([^\" ]+)') +menu_file_regex = re.compile(r'^/usr/lib/menu/([^/]+)$') +old_menu_file_regex = re.compile(r'^/usr/share/(gnome/apps|applnk)/([^/]+)$') +package_regex = re.compile(r'\?package\((.*)\):') +needs_regex = re.compile(r'needs=(\"([^\"]+)\"|([^ %s\"]+))' % "\t") +section_regex = re.compile(r'section=(\"([^\"]+)\"|([^ %s\"]+))' % "\t") +title_regex = re.compile(r'[\"\s]title=(\"([^\"]+)\"|([^ %s\"]+))' % "\t") +longtitle_regex = re.compile(r'longtitle=(\"([^\"]+)\"|([^ %s\"]+))' % "\t") +command_regex = re.compile(r'command=(?:\"([^\"]+)\"|([^ %s\"]+))' % "\t") +icon_regex = re.compile(r'icon=\"?([^\" ]+)') valid_sections = Config.getOption('ValidMenuSections', DEFAULT_VALID_SECTIONS) -update_menus_regex = re.compile('^[^#]*update-menus', re.MULTILINE) +update_menus_regex = re.compile(r'^[^#]*update-menus', re.MULTILINE) standard_needs = Config.getOption('ExtraMenuNeeds', DEFAULT_EXTRA_MENU_NEEDS) icon_paths = Config.getOption('IconPath', DEFAULT_ICON_PATH) -xpm_ext_regex = re.compile('/usr/share/icons/(mini/|large/).*\.xpm$') -icon_ext_regex = re.compile(Config.getOption('IconFilename', '.*\.png$')) -version_regex = re.compile('([0-9.][0-9.]+)($|\s)') +xpm_ext_regex = re.compile(r'/usr/share/icons/(mini/|large/).*\.xpm$') +icon_ext_regex = re.compile(Config.getOption('IconFilename', r'.*\.png$')) +version_regex = re.compile(r'([0-9.][0-9.]+)($|\s)') launchers = Config.getOption('MenuLaunchers', DEFAULT_LAUNCHERS) -xdg_migrated_regex = re.compile('xdg=\"?([^\" ]+)') +xdg_migrated_regex = re.compile(r'xdg=\"?([^\" ]+)') # compile regexps for l in launchers: @@ -452,5 +452,3 @@ sizes of the icon from being found.''', ) # MenuCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/MenuXDGCheck.py b/MenuXDGCheck.py index df84738..2daab51 100644 --- a/MenuXDGCheck.py +++ b/MenuXDGCheck.py @@ -25,7 +25,7 @@ class MenuXDGCheck(AbstractCheck.AbstractFilesCheck): # $ echo $XDG_DATA_DIRS/applications # /var/lib/menu-xdg:/usr/share AbstractCheck.AbstractFilesCheck.__init__( - self, "MenuXDGCheck", "/usr/share/applications/.*\.desktop$") + self, "MenuXDGCheck", r"/usr/share/applications/.*\.desktop$") def check_file(self, pkg, filename): root = pkg.dirName() @@ -63,6 +63,7 @@ class MenuXDGCheck(AbstractCheck.AbstractFilesCheck): printWarning(pkg, 'desktopfile-without-binary', filename, binary) + check = MenuXDGCheck() addDetails( @@ -76,5 +77,3 @@ addDetails( '''the .desktop file is for a file not present in the package. You should check the requires or see if this is not a error''', ) - -# ex: ts=4 sw=4 et diff --git a/NamingPolicyCheck.py b/NamingPolicyCheck.py index e3ac9b2..0837f83 100644 --- a/NamingPolicyCheck.py +++ b/NamingPolicyCheck.py @@ -13,7 +13,6 @@ import re import AbstractCheck from Filter import addDetails, printWarning - # could be added. # # zope @@ -26,7 +25,7 @@ from Filter import addDetails, printWarning # XFree # xine -simple_naming_policy_re = re.compile('\^[a-zA-Z1-9-_]*$') +simple_naming_policy_re = re.compile(r'\^[a-zA-Z1-9-_]*$') class NamingPolicyNotAppliedException(Exception): @@ -69,6 +68,7 @@ class NamingPolicyCheck(AbstractCheck.AbstractCheck): except NamingPolicyNotAppliedException: printWarning(pkg, c['pkg_name'] + '-naming-policy-not-applied', f) + check = NamingPolicyCheck() # @@ -88,7 +88,6 @@ check = NamingPolicyCheck() # the naming file. # if somone as a elegant solution, I will be happy to implement and test it. - check.add_check('xmms', '^xmms(-|$)', '^/usr/lib(64)?/xmms/') check.add_check('python', '^python(-|$)', '^/usr/lib(64)?/python[1-9](-[1-9])?') check.add_check('perl5', '^perl(-|$)', '^/usr/lib(64)?/perl5/vendor_perl') @@ -105,5 +104,3 @@ check.add_check('ocaml', '^ocaml(-|$)', '/usr/lib(64)?/ocaml/') # ruby # NamingPolicyCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/PamCheck.py b/PamCheck.py index aec8689..0aa586c 100644 --- a/PamCheck.py +++ b/PamCheck.py @@ -14,13 +14,13 @@ import AbstractCheck from Filter import addDetails, printError -pam_stack_re = re.compile('^\s*[^#].*pam_stack\.so\s*service') +pam_stack_re = re.compile(r'^\s*[^#].*pam_stack\.so\s*service') class PamCheck(AbstractCheck.AbstractFilesCheck): def __init__(self): AbstractCheck.AbstractFilesCheck.__init__(self, "PamCheck", - "/etc/pam\.d/.*") + r"/etc/pam\.d/.*") def check_file(self, pkg, filename): lines = pkg.grep(pam_stack_re, filename) @@ -28,11 +28,10 @@ class PamCheck(AbstractCheck.AbstractFilesCheck): printError(pkg, 'use-old-pam-stack', filename, '(line %s)' % ", ".join(lines)) + check = PamCheck() addDetails( 'use-old-pam-stack', '''Update pam file to use include instead of pam_stack.''', ) - -# ex: ts=4 sw=4 et diff --git a/Pkg.py b/Pkg.py index 7985323..e257243 100644 --- a/Pkg.py +++ b/Pkg.py @@ -41,8 +41,8 @@ if sys.version_info[0] > 2: unicode = str def b2s(b): - if b is None: - return None + if b is None or isinstance(b, str): + return b if isinstance(b, (list, tuple)): return [b2s(x) for x in b] return b.decode(errors='replace') @@ -63,6 +63,7 @@ def warn(s): """ print(s, file=sys.stderr) + # 64: RPMSENSE_PREREQ is 0 with rpm 4.4..4.7, we want 64 here in order # to do the right thing with those versions and packages built with other # rpm versions @@ -88,12 +89,12 @@ SCRIPT_TAGS = [ '%transfiletrigger'), ] -var_regex = re.compile('^(.*)\${?(\w+)}?(.*)$') +var_regex = re.compile(r'^(.*)\${?(\w+)}?(.*)$') def shell_var_value(var, script): - assign_regex = re.compile('\\b' + re.escape(var) + '\s*=\s*(.+)\s*(#.*)*$', - re.MULTILINE) + assign_regex = re.compile(r'\b' + re.escape(var) + + r'\s*=\s*(.+)\s*(#.*)*$', re.MULTILINE) res = assign_regex.search(script) if res: res2 = var_regex.search(res.group(1)) @@ -117,17 +118,18 @@ def substitute_shell_vars(val, script): return val -def getstatusoutput(cmd, stdoutonly=False, shell=False, raw=False): +def getstatusoutput(cmd, stdoutonly=False, shell=False, raw=False, lc_all="C"): """ A version of commands.getstatusoutput() which can take cmd as a sequence, thus making it potentially more secure. """ + env = dict(os.environ, LC_ALL=lc_all) if stdoutonly: proc = subprocess.Popen(cmd, shell=shell, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, close_fds=True) + stdout=subprocess.PIPE, close_fds=True, env=env) else: proc = subprocess.Popen(cmd, shell=shell, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, + stdout=subprocess.PIPE, env=env, stderr=subprocess.STDOUT, close_fds=True) proc.stdin.close() with proc.stdout: @@ -141,8 +143,9 @@ def getstatusoutput(cmd, stdoutonly=False, shell=False, raw=False): sts = 0 return sts, text -bz2_regex = re.compile('\.t?bz2?$') -xz_regex = re.compile('\.(t[xl]z|xz|lzma)$') + +bz2_regex = re.compile(r'\.t?bz2?$') +xz_regex = re.compile(r'\.(t[xl]z|xz|lzma)$') def catcmd(fname): @@ -195,9 +198,10 @@ def mktemp(): tmpfile = os.fdopen(tmpfd, 'w') return tmpfile, tmpname -slash_regex = re.compile('/+') -slashdot_regex = re.compile('/(\.(/|$))+') -slashend_regex = re.compile('([^/])/+$') + +slash_regex = re.compile(r'/+') +slashdot_regex = re.compile(r'/(\.(/|$))+') +slashend_regex = re.compile(r'([^/])/+$') def safe_normpath(path): @@ -402,7 +406,7 @@ def parse_deps(line): """ prcos = [] - tokens = re.split('[\s,]+', line.strip()) + tokens = re.split(r'[\s,]+', line.strip()) # Drop line continuation backslash in multiline macro definition (for # spec file parsing), e.g. @@ -476,7 +480,7 @@ class AbstractPkg(object): class Pkg(AbstractPkg): - _magic_from_compressed_re = re.compile('\([^)]+\s+compressed\s+data\\b') + _magic_from_compressed_re = re.compile(r'\([^)]+\s+compressed\s+data\b') def __init__(self, filename, dirname, header=None, is_source=False): self.filename = filename @@ -538,7 +542,8 @@ class Pkg(AbstractPkg): if key in (rpm.RPMTAG_NAME, rpm.RPMTAG_VERSION, rpm.RPMTAG_RELEASE, rpm.RPMTAG_ARCH, rpm.RPMTAG_GROUP, rpm.RPMTAG_BUILDHOST, rpm.RPMTAG_LICENSE, rpm.RPMTAG_HEADERI18NTABLE, - rpm.RPMTAG_PACKAGER, rpm.RPMTAG_SOURCERPM) \ + rpm.RPMTAG_PACKAGER, rpm.RPMTAG_SOURCERPM, + rpm.RPMTAG_DISTRIBUTION, rpm.RPMTAG_VENDOR) \ or key in (x[0] for x in SCRIPT_TAGS) \ or key in (x[1] for x in SCRIPT_TAGS): val = b2s(val) @@ -569,7 +574,7 @@ class Pkg(AbstractPkg): return cmd def checkSignature(self): - return getstatusoutput(('env', 'LC_ALL=C', 'rpm', '-K', self.filename)) + return getstatusoutput(('rpm', '-K', self.filename)) # remove the extracted files from the package def cleanup(self): @@ -781,7 +786,7 @@ class Pkg(AbstractPkg): def check_versioned_dep(self, name, version): # try to match name%_isa as well (e.g. "foo(x86-64)", "foo(x86-32)") - name_re = re.compile('^%s(\(\w+-\d+\))?$' % re.escape(name)) + name_re = re.compile(r'^%s(\(\w+-\d+\))?$' % re.escape(name)) for d in self.requires() + self.prereq(): if name_re.match(d[0]): if d[1] & rpm.RPMSENSE_EQUAL != rpm.RPMSENSE_EQUAL \ @@ -924,7 +929,7 @@ def getInstalledPkgs(name): pkgs = [] ts = rpm.TransactionSet() - if re.search('[?*]|\[.+\]', name): + if re.search(r'[?*]|\[.+\]', name): mi = ts.dbMatch() mi.pattern("name", rpm.RPMMIRE_GLOB, name) else: @@ -1014,5 +1019,3 @@ if __name__ == '__main__': print('Obsoletes: %s' % pkg.obsoletes()) # Pkg.py ends here - -# ex: ts=4 sw=4 et diff --git a/PostCheck.py b/PostCheck.py index 6188a9a..1dc4d2a 100644 --- a/PostCheck.py +++ b/PostCheck.py @@ -43,15 +43,15 @@ empty_shells = Config.getOption('ValidEmptyShells', DEFAULT_EMPTY_SHELLS) # shells that grok the -n switch for debugging syntaxcheck_shells = ('/bin/sh', '/bin/bash') -percent_regex = re.compile('^[^#]*%{?\w{3,}', re.MULTILINE) -bracket_regex = re.compile('^[^#]*if\s+[^ :\]]\]', re.MULTILINE) -home_regex = re.compile('[^a-zA-Z]+~/|\${?HOME(\W|$)', re.MULTILINE) -dangerous_command_regex = re.compile("(^|[;\|`]|&&|$\()\s*(?:\S*/s?bin/)?(cp|mv|ln|tar|rpm|chmod|chown|rm|cpio|install|perl|userdel|groupdel)\s", re.MULTILINE) -selinux_regex = re.compile("(^|[;\|`]|&&|$\()\s*(?:\S*/s?bin/)?(chcon|runcon)\s", re.MULTILINE) -single_command_regex = re.compile("^[ \n]*([^ \n]+)[ \n]*$") -tmp_regex = re.compile('^[^#]*\s(/var)?/tmp', re.MULTILINE) -menu_regex = re.compile('^/usr/lib/menu/|^/etc/menu-methods/|^/usr/share/applications/') -bogus_var_regex = re.compile('(\${?RPM_BUILD_(ROOT|DIR)}?)') +percent_regex = re.compile(r'^[^#]*%{?\w{3,}', re.MULTILINE) +bracket_regex = re.compile(r'^[^#]*if\s+[^ :\]]\]', re.MULTILINE) +home_regex = re.compile(r'[^a-zA-Z]+~/|\${?HOME(\W|$)', re.MULTILINE) +dangerous_command_regex = re.compile(r"(^|[;\|`]|&&|$\()\s*(?:\S*/s?bin/)?(cp|mv|ln|tar|rpm|chmod|chown|rm|cpio|install|perl|userdel|groupdel)\s", re.MULTILINE) +selinux_regex = re.compile(r"(^|[;\|`]|&&|$\()\s*(?:\S*/s?bin/)?(chcon|runcon)\s", re.MULTILINE) +single_command_regex = re.compile(r"^[ %s]*([^ %s]+)[ %s]*$" % (("\n",) * 3)) +tmp_regex = re.compile(r'^[^#]*\s(/var)?/tmp', re.MULTILINE) +menu_regex = re.compile(r'^/usr/lib/menu/|^/etc/menu-methods/|^/usr/share/applications/') +bogus_var_regex = re.compile(r'(\${?RPM_BUILD_(ROOT|DIR)}?)') prereq_assoc = ( # ['chkconfig', ('chkconfig', '/sbin/chkconfig')], @@ -60,7 +60,7 @@ prereq_assoc = ( ) for p in prereq_assoc: - p[0] = re.compile('^[^#]+' + p[0], re.MULTILINE) + p[0] = re.compile(r'^[^#]+' + p[0], re.MULTILINE) # pychecker fix del p @@ -190,6 +190,7 @@ class PostCheck(AbstractCheck.AbstractCheck): elif prog not in empty_shells and prog in valid_shells: printWarning(pkg, 'empty-' + tag) + # Create an object to enable the auto registration of the test check = PostCheck() @@ -233,5 +234,3 @@ and clean up the scriptlet contents if appropriate.''', ) # PostCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/RpmFileCheck.py b/RpmFileCheck.py index 4ad1f90..da43df4 100644 --- a/RpmFileCheck.py +++ b/RpmFileCheck.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # check the rpm file for various errors. - +# # Copyright (C) 2006 Michael Scherer # Ville Skyttä # @@ -34,6 +34,7 @@ class RpmFileCheck(AbstractCheck.AbstractCheck): if len(rpmfile_name) > 64: printWarning(pkg, 'filename-too-long-for-joliet', rpmfile_name) + check = RpmFileCheck() addDetails( @@ -41,5 +42,3 @@ addDetails( '''This filename is too long to fit on a joliet filesystem (limit is 64 unicode chars).''', ) - -# ex: ts=4 sw=4 et diff --git a/SCLCheck.py b/SCLCheck.py index eb4d4c9..24b635a 100644 --- a/SCLCheck.py +++ b/SCLCheck.py @@ -68,7 +68,7 @@ class SCLCheck(AbstractCheck.AbstractCheck): self._spec_file = pkgfile.path self.check_spec(pkg, self._spec_file) - def check_spec(self, pkg, spec_file): + def check_spec(self, pkg, spec_file, spec_lines=None): '''SCL spec file checks''' spec = '\n'.join(Pkg.readlines(spec_file)) if global_scl_definition.search(spec): diff --git a/SignatureCheck.py b/SignatureCheck.py index ead21d6..483579d 100644 --- a/SignatureCheck.py +++ b/SignatureCheck.py @@ -15,8 +15,8 @@ import Pkg class SignatureCheck(AbstractCheck.AbstractCheck): - pgp_regex = re.compile("pgp|gpg", re.IGNORECASE) - unknown_key_regex = re.compile("\(MISSING KEYS:(?:\([^)]+\))?\s+([^\)]+)\)") + pgp_regex = re.compile(r"pgp|gpg", re.IGNORECASE) + unknown_key_regex = re.compile(r"\(MISSING KEYS:(?:\([^)]+\))?\s+([^\)]+)\)") def __init__(self): AbstractCheck.AbstractCheck.__init__(self, "SignatureCheck") @@ -37,6 +37,7 @@ class SignatureCheck(AbstractCheck.AbstractCheck): if not SignatureCheck.pgp_regex.search(res[1]): printError(pkg, "no-signature") + # Create an object to enable the auto registration of the test check = SignatureCheck() @@ -51,5 +52,3 @@ See the rpm --import option for more information.''', ) # SignatureCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/SourceCheck.py b/SourceCheck.py index 83d7bdb..ee22aa9 100644 --- a/SourceCheck.py +++ b/SourceCheck.py @@ -16,7 +16,7 @@ from Filter import addDetails, printError, printWarning DEFAULT_VALID_SRC_PERMS = (0o644, 0o755) -source_regex = re.compile('\\.(tar|patch|tgz|diff)$') +source_regex = re.compile(r'\\.(tar|patch|tgz|diff)$') compress_ext = Config.getOption("CompressExtension", "bz2") valid_src_perms = Config.getOption("ValidSrcPerms", DEFAULT_VALID_SRC_PERMS) @@ -43,6 +43,7 @@ class SourceCheck(AbstractCheck.AbstractCheck): if perm not in valid_src_perms: printWarning(pkg, 'strange-permission', fname, "%o" % perm) + check = SourceCheck() addDetails( @@ -63,5 +64,3 @@ permissions. Usually, a file should have 0644 permissions.''', ) # SourceCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/SpecCheck.py b/SpecCheck.py index 568ec7c..8cb85b3 100644 --- a/SpecCheck.py +++ b/SpecCheck.py @@ -30,40 +30,42 @@ DEFAULT_BIARCH_PACKAGES = '^(gcc|glibc)' # Don't check for hardcoded library paths in packages which can have # their noarch files in /usr/lib//*, or packages that can't # be installed on biarch systems -DEFAULT_HARDCODED_LIB_PATH_EXCEPTIONS = '/lib/(modules|cpp|perl5|rpm|hotplug|firmware)($|[\s/,])' +DEFAULT_HARDCODED_LIB_PATH_EXCEPTIONS = r'/lib/(modules|cpp|perl5|rpm|hotplug|firmware|systemd)($|[\s/,])' def re_tag_compile(tag): - r = "^%s\s*:\s*(\S.*?)\s*$" % tag + r = r"^%s\s*:\s*(\S.*?)\s*$" % tag return re.compile(r, re.IGNORECASE) -patch_regex = re_tag_compile('Patch(\d*)') -applied_patch_regex = re.compile("^%patch(\d*)") -applied_patch_p_regex = re.compile("\s-P\s+(\d+)\\b") + +patch_regex = re_tag_compile(r'Patch(\d*)') +applied_patch_regex = re.compile(r"^%patch(\d*)") +applied_patch_p_regex = re.compile(r"\s-P\s+(\d+)\b") applied_patch_pipe_regex = re.compile(r'\s%\{PATCH(\d+)\}\s*\|\s*(%\{?__)?patch\b') -source_dir_regex = re.compile("^[^#]*(\$RPM_SOURCE_DIR|%{?_sourcedir}?)") -obsolete_tags_regex = re_tag_compile('(?:Serial|Copyright)') +applied_patch_i_regex = re.compile(r"(?:%\{?__)?patch\}?.*?\s+(?:<|-i)\s+%\{PATCH(\d+)\}") +source_dir_regex = re.compile(r"^[^#]*(\$RPM_SOURCE_DIR|%{?_sourcedir}?)") +obsolete_tags_regex = re_tag_compile(r'(?:Serial|Copyright)') buildroot_regex = re_tag_compile('BuildRoot') prefix_regex = re_tag_compile('Prefix') packager_regex = re_tag_compile('Packager') buildarch_regex = re_tag_compile('BuildArch(?:itectures)?') buildprereq_regex = re_tag_compile('BuildPreReq') -prereq_regex = re_tag_compile('PreReq(\(.*\))') - -make_check_regex = re.compile('(^|\s|%{?__)make}?\s+(check|test)') -rm_regex = re.compile('(^|\s)((.*/)?rm|%{?__rm}?) ') -rpm_buildroot_regex = re.compile('^[^#]*(?:(\\\*)\${?RPM_BUILD_ROOT}?|(%+){?buildroot}?)') -configure_libdir_spec_regex = re.compile('ln |\./configure[^#]*--libdir=(\S+)[^#]*') -lib_package_regex = re.compile('^%package.*\Wlib') -ifarch_regex = re.compile('^\s*%ifn?arch\s') -if_regex = re.compile('^\s*%if\s') -endif_regex = re.compile('^\s*%endif\\b') +prereq_regex = re_tag_compile(r'PreReq(\(.*\))') + +make_check_regex = re.compile(r'(^|\s|%{?__)make}?\s+(check|test)') +rm_regex = re.compile(r'(^|\s)((.*/)?rm|%{?__rm}?) ') +rpm_buildroot_regex = re.compile(r'^[^#]*(?:(\\\*)\${?RPM_BUILD_ROOT}?|(%+){?buildroot}?)') +configure_libdir_spec_regex = re.compile(r'ln |\./configure[^#]*--libdir=(\S+)[^#]*') +lib_package_regex = re.compile(r'^%package.*\Wlib') +ifarch_regex = re.compile(r'^\s*%ifn?arch\s') +if_regex = re.compile(r'^\s*%if\s') +endif_regex = re.compile(r'^\s*%endif\b') biarch_package_regex = re.compile(DEFAULT_BIARCH_PACKAGES) hardcoded_lib_path_exceptions_regex = re.compile(Config.getOption('HardcodedLibPathExceptions', DEFAULT_HARDCODED_LIB_PATH_EXCEPTIONS)) use_utf8 = Config.getOption('UseUTF8', Config.USEUTF8_DEFAULT) -libdir_regex = re.compile('%{?_lib(?:dir)?\}?\\b') +libdir_regex = re.compile(r'%{?_lib(?:dir)?\}?\b') section_regexs = dict( - ([x, re.compile('^%' + x + '(?:\s|$)')] + ([x, re.compile('^%' + x + r'(?:\s|$)')] for x in ('build', 'changelog', 'check', 'clean', 'description', 'files', 'install', 'package', 'prep') + RPM_SCRIPTLETS)) deprecated_grep_regex = re.compile(r'\b[ef]grep\b') @@ -71,38 +73,38 @@ deprecated_grep_regex = re.compile(r'\b[ef]grep\b') # Only check for /lib, /usr/lib, /usr/X11R6/lib # TODO: better handling of X libraries and modules. hardcoded_library_paths = '(/lib|/usr/lib|/usr/X11R6/lib/(?!([^/]+/)+)[^/]*\\.([oa]|la|so[0-9.]*))' -hardcoded_library_path_regex = re.compile('^[^#]*((^|\s+|\.\./\.\.|\${?RPM_BUILD_ROOT}?|%{?buildroot}?|%{?_prefix}?)' + hardcoded_library_paths + '(?=[\s;/])([^\s,;]*))') +hardcoded_library_path_regex = re.compile(r'^[^#]*((^|\s+|\.\./\.\.|\${?RPM_BUILD_ROOT}?|%{?buildroot}?|%{?_prefix}?)' + hardcoded_library_paths + r'(?=[\s;/])([^\s,;]*))') # Requires(pre,post) is broken in some rpm versions, see # https://bugzilla.redhat.com/118780 and bugs linked to that one. -scriptlet_requires_regex = re.compile('^(PreReq|Requires)\([^\)]*,', re.IGNORECASE) +scriptlet_requires_regex = re.compile(r'^(PreReq|Requires)\([^\)]*,', re.IGNORECASE) -DEFINE_RE = '(^|\s)%(define|global)\s+' -depscript_override_regex = re.compile(DEFINE_RE + '__find_(requires|provides)\s') -depgen_disable_regex = re.compile(DEFINE_RE + '_use_internal_dependency_generator\s+0') -patch_fuzz_override_regex = re.compile(DEFINE_RE + '_default_patch_fuzz\s+(\d+)') +DEFINE_RE = r'(^|\s)%(define|global)\s+' +depscript_override_regex = re.compile(DEFINE_RE + r'__find_(requires|provides)\s') +depgen_disable_regex = re.compile(DEFINE_RE + r'_use_internal_dependency_generator\s+0') +patch_fuzz_override_regex = re.compile(DEFINE_RE + r'_default_patch_fuzz\s+(\d+)') # See https://bugzilla.redhat.com/488146 for details indent_spaces_regex = re.compile('( \t|(^|\t)([^\t]{8})*[^\t]{4}[^\t]?([^\t][^\t.!?]|[^\t]?[.!?] ) )') -requires_regex = re.compile('^(?:Build)?(?:Pre)?Req(?:uires)?(?:\([^\)]+\))?:\s*(.*)', re.IGNORECASE) -provides_regex = re.compile('^Provides(?:\([^\)]+\))?:\s*(.*)', re.IGNORECASE) -obsoletes_regex = re.compile('^Obsoletes:\s*(.*)', re.IGNORECASE) -conflicts_regex = re.compile('^(?:Build)?Conflicts:\s*(.*)', re.IGNORECASE) +requires_regex = re.compile(r'^(?:Build)?(?:Pre)?Req(?:uires)?(?:\([^\)]+\))?:\s*(.*)', re.IGNORECASE) +provides_regex = re.compile(r'^Provides(?:\([^\)]+\))?:\s*(.*)', re.IGNORECASE) +obsoletes_regex = re.compile(r'^Obsoletes:\s*(.*)', re.IGNORECASE) +conflicts_regex = re.compile(r'^(?:Build)?Conflicts:\s*(.*)', re.IGNORECASE) -compop_regex = re.compile('[<>=]') +compop_regex = re.compile(r'[<>=]') setup_regex = re.compile(r'%setup\b') # intentionally no whitespace before! -setup_q_regex = re.compile(' -[A-Za-z]*q') -setup_t_regex = re.compile(' -[A-Za-z]*T') -setup_ab_regex = re.compile(' -[A-Za-z]*[ab]') -autosetup_regex = re.compile('^\s*%autosetup(\s.*|$)') -autosetup_n_regex = re.compile(' -[A-Za-z]*N') -autopatch_regex = re.compile('^\s*%autopatch(?:\s|$)') +setup_q_regex = re.compile(r' -[A-Za-z]*q') +setup_t_regex = re.compile(r' -[A-Za-z]*T') +setup_ab_regex = re.compile(r' -[A-Za-z]*[ab]') +autosetup_regex = re.compile(r'^\s*%autosetup(\s.*|$)') +autosetup_n_regex = re.compile(r' -[A-Za-z]*N') +autopatch_regex = re.compile(r'^\s*%autopatch(?:\s|$)') -filelist_regex = re.compile('\s+-f\s+\S+') -pkgname_regex = re.compile('\s+(?:-n\s+)?(\S+)') -tarball_regex = re.compile('\.(?:t(?:ar|[glx]z|bz2?)|zip)\\b', re.IGNORECASE) +filelist_regex = re.compile(r'\s+-f\s+\S+') +pkgname_regex = re.compile(r'\s+(?:-n\s+)?(\S+)') +tarball_regex = re.compile(r'\.(?:t(?:ar|[glx]z|bz2?)|zip)\b', re.IGNORECASE) UNICODE_NBSP = unicodedata.lookup('NO-BREAK SPACE') @@ -129,6 +131,7 @@ class SpecCheck(AbstractCheck.AbstractCheck): def __init__(self): AbstractCheck.AbstractCheck.__init__(self, "SpecCheck") self._spec_file = None + self._spec_name = None def check_source(self, pkg): wrong_spec = False @@ -137,6 +140,7 @@ class SpecCheck(AbstractCheck.AbstractCheck): for fname, pkgfile in pkg.files().items(): if fname.endswith('.spec'): self._spec_file = pkgfile.path + self._spec_name = pkgfile.name if fname == pkg.name + ".spec": wrong_spec = False break @@ -151,9 +155,11 @@ class SpecCheck(AbstractCheck.AbstractCheck): # check content of spec file self.check_spec(pkg, self._spec_file) - def check_spec(self, pkg, spec_file): + def check_spec(self, pkg, spec_file, spec_lines=None): self._spec_file = spec_file spec_only = isinstance(pkg, Pkg.FakePkg) + if not spec_lines: + spec_lines = Pkg.readlines(spec_file) patches = {} applied_patches = [] applied_patches_ifarch = [] @@ -183,18 +189,17 @@ class SpecCheck(AbstractCheck.AbstractCheck): if Pkg.is_utf8(self._spec_file): is_utf8 = True else: - printError(pkg, "non-utf8-spec-file", self._spec_file) + printError(pkg, "non-utf8-spec-file", + self._spec_name or self._spec_file) # gather info from spec lines pkg.current_linenum = 0 - nbsp = chr(0xA0) - if is_utf8: - nbsp = UNICODE_NBSP + nbsp = UNICODE_NBSP if is_utf8 else chr(0xA0) do_unicode = is_utf8 and sys.version_info[0] <= 2 - for line in Pkg.readlines(spec_file): + for line in spec_lines: pkg.current_linenum += 1 @@ -293,6 +298,13 @@ class SpecCheck(AbstractCheck.AbstractCheck): applied_patches.append(pnum) if ifarch_depth > 0: applied_patches_ifarch.append(pnum) + else: + res = applied_patch_i_regex.search(line) + if res: + pnum = int(res.group(1)) + applied_patches.append(pnum) + if ifarch_depth > 0: + applied_patches_ifarch.append(pnum) if not res and not source_dir: res = source_dir_regex.search(line) if res: @@ -404,8 +416,9 @@ class SpecCheck(AbstractCheck.AbstractCheck): if res: provs = Pkg.parse_deps(res.group(1)) for prov in unversioned(provs): - printWarning(pkg, 'unversioned-explicit-provides', - prov) + if not prov.startswith('/'): + printWarning(pkg, 'unversioned-explicit-provides', + prov) if compop_regex.search(prov): printWarning(pkg, 'comparison-operator-in-deptoken', @@ -415,8 +428,9 @@ class SpecCheck(AbstractCheck.AbstractCheck): if res: obses = Pkg.parse_deps(res.group(1)) for obs in unversioned(obses): - printWarning(pkg, 'unversioned-explicit-obsoletes', - obs) + if not obs.startswith('/'): + printWarning(pkg, 'unversioned-explicit-obsoletes', + obs) if compop_regex.search(obs): printWarning(pkg, 'comparison-operator-in-deptoken', @@ -540,8 +554,8 @@ class SpecCheck(AbstractCheck.AbstractCheck): # but it seems errors from rpmlib get logged to stderr and we can't # capture and print them nicely, so we do it once each way :P - out = Pkg.getstatusoutput(('env', 'LC_ALL=C', 'rpm', '-q', - '--qf=', '--specfile', self._spec_file)) + out = Pkg.getstatusoutput( + ('rpm', '-q', '--qf=', '--specfile', self._spec_file)) parse_error = False for line in out[1].splitlines(): # No such file or dir hack: https://bugzilla.redhat.com/487855 @@ -606,6 +620,7 @@ class SpecCheck(AbstractCheck.AbstractCheck): elif srctype == "Source" and tarball_regex.search(url): printWarning(pkg, 'invalid-url', '%s:' % tag, url) + # Create an object to enable the auto registration of the test check = SpecCheck() @@ -828,5 +843,3 @@ upstream.''' ) # SpecCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/TagsCheck.py b/TagsCheck.py index f5b7516..a8d87aa 100644 --- a/TagsCheck.py +++ b/TagsCheck.py @@ -398,7 +398,7 @@ BAD_WORDS = { } DEFAULT_INVALID_REQUIRES = ('^is$', '^not$', '^owned$', '^by$', '^any$', - '^package$', '^libsafe\.so\.') + '^package$', r'^libsafe\.so\.') VALID_GROUPS = Config.getOption('ValidGroups', None) if VALID_GROUPS is None: # get defaults from rpm package only if it's not set @@ -406,25 +406,25 @@ if VALID_GROUPS is None: # get defaults from rpm package only if it's not set VALID_LICENSES = Config.getOption('ValidLicenses', DEFAULT_VALID_LICENSES) INVALID_REQUIRES = map(re.compile, Config.getOption('InvalidRequires', DEFAULT_INVALID_REQUIRES)) packager_regex = re.compile(Config.getOption('Packager')) -changelog_version_regex = re.compile('[^>]([^ >]+)\s*$') -changelog_text_version_regex = re.compile('^\s*-\s*((\d+:)?[\w\.]+-[\w\.]+)') +changelog_version_regex = re.compile(r'[^>]([^ >]+)\s*$') +changelog_text_version_regex = re.compile(r'^\s*-\s*((\d+:)?[\w\.]+-[\w\.]+)') release_ext = Config.getOption('ReleaseExtension') extension_regex = release_ext and re.compile(release_ext) use_version_in_changelog = Config.getOption('UseVersionInChangelog', True) -devel_number_regex = re.compile('(.*?)([0-9.]+)(_[0-9.]+)?-devel') -lib_devel_number_regex = re.compile('^lib(.*?)([0-9.]+)(_[0-9.]+)?-devel') +devel_number_regex = re.compile(r'(.*?)([0-9.]+)(_[0-9.]+)?-devel') +lib_devel_number_regex = re.compile(r'^lib(.*?)([0-9.]+)(_[0-9.]+)?-devel') invalid_url_regex = re.compile(Config.getOption('InvalidURL'), re.IGNORECASE) -lib_package_regex = re.compile('(?:^(?:compat-)?lib.*?(\.so.*)?|libs?[\d-]*)$', re.IGNORECASE) -leading_space_regex = re.compile('^\s+') -license_regex = re.compile('\(([^)]+)\)|\s(?:and|or)\s') -invalid_version_regex = re.compile('([0-9](?:rc|alpha|beta|pre).*)', re.IGNORECASE) +lib_package_regex = re.compile(r'(?:^(?:compat-)?lib.*?(\.so.*)?|libs?[\d-]*)$', re.IGNORECASE) +leading_space_regex = re.compile(r'^\s+') +license_regex = re.compile(r'\(([^)]+)\)|\s(?:and|or|AND|OR)\s') +invalid_version_regex = re.compile(r'([0-9](?:rc|alpha|beta|pre).*)', re.IGNORECASE) # () are here for grouping purpose in the regexp -forbidden_words_regex = re.compile('(' + Config.getOption('ForbiddenWords') + ')', re.IGNORECASE) +forbidden_words_regex = re.compile(r'(%s)' % Config.getOption('ForbiddenWords'), re.IGNORECASE) valid_buildhost_regex = re.compile(Config.getOption('ValidBuildHost')) use_epoch = Config.getOption('UseEpoch', False) use_utf8 = Config.getOption('UseUTF8', Config.USEUTF8_DEFAULT) max_line_len = Config.getOption('MaxLineLength', 79) -tag_regex = re.compile('^((?:Auto(?:Req|Prov|ReqProv)|Build(?:Arch(?:itectures)?|Root)|(?:Build)?Conflicts|(?:Build)?(?:Pre)?Requires|Copyright|(?:CVS|SVN)Id|Dist(?:ribution|Tag|URL)|DocDir|(?:Build)?Enhances|Epoch|Exclu(?:de|sive)(?:Arch|OS)|Group|Icon|License|Name|No(?:Patch|Source)|Obsoletes|Packager|Patch\d*|Prefix(?:es)?|Provides|(?:Build)?Recommends|Release|RHNPlatform|Serial|Source\d*|(?:Build)?Suggests|Summary|(?:Build)?Supplements|(?:Bug)?URL|Vendor|Version)(?:\([^)]+\))?:)\s*\S', re.IGNORECASE) +tag_regex = re.compile(r'^((?:Auto(?:Req|Prov|ReqProv)|Build(?:Arch(?:itectures)?|Root)|(?:Build)?Conflicts|(?:Build)?(?:Pre)?Requires|Copyright|(?:CVS|SVN)Id|Dist(?:ribution|Tag|URL)|DocDir|(?:Build)?Enhances|Epoch|Exclu(?:de|sive)(?:Arch|OS)|Group|Icon|License|Name|No(?:Patch|Source)|Obsoletes|Packager|Patch\d*|Prefix(?:es)?|Provides|(?:Build)?Recommends|Release|RHNPlatform|Serial|Source\d*|(?:Build)?Suggests|Summary|(?:Build)?Supplements|(?:Bug)?URL|Vendor|Version)(?:\([^)]+\))?:)\s*\S', re.IGNORECASE) punct = '.,:;!?' sentence_break_regex = re.compile(r'(^|[.:;!?])\s*$') so_dep_regex = re.compile(r'\.so(\.[0-9a-zA-z]+)*(\([^)]*\))*$') @@ -515,7 +515,7 @@ def spell_check(pkg, str, fmt, lang, ignored): if not enchant or not dict_found: for seq in str.split(): - for word in re.split('[^a-z]+', seq.lower()): + for word in re.split(r'[^a-z]+', seq.lower()): if len(word) == 0: continue correct = BAD_WORDS.get(word) @@ -643,7 +643,7 @@ class TagsCheck(AbstractCheck.AbstractCheck): base_or_libs = base + '/' + base + '-libs/lib' + base # try to match *%_isa as well (e.g. "(x86-64)", "(x86-32)") base_or_libs_re = re.compile( - '^(lib)?%s(-libs)?(\(\w+-\d+\))?$' % re.escape(base)) + r'^(lib)?%s(-libs)?(\(\w+-\d+\))?$' % re.escape(base)) for d in deps: if base_or_libs_re.match(d[0]): dep = d @@ -919,8 +919,8 @@ class TagsCheck(AbstractCheck.AbstractCheck): if res and Config.getOption('ForbiddenWords'): printWarning(pkg, 'summary-use-invalid-word', lang, res.group(1)) if pkg.name: - sepchars = '[\s' + punct + ']' - res = re.search('(?:^|\s)(%s)(?:%s|$)' % + sepchars = r'[\s%s]' % punct + res = re.search(r'(?:^|\s)(%s)(?:%s|$)' % (re.escape(pkg.name), sepchars), summary, re.IGNORECASE | re.UNICODE) if res: @@ -1146,5 +1146,3 @@ for i in ("obsoletes", "conflicts", "provides", "recommends", "suggests", % i.capitalize()) # TagsCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/ZipCheck.py b/ZipCheck.py index 59c59bc..ef75347 100644 --- a/ZipCheck.py +++ b/ZipCheck.py @@ -19,9 +19,9 @@ from Filter import addDetails, printError, printWarning import Pkg -zip_regex = re.compile('\.(zip|[ewj]ar)$') -jar_regex = re.compile('\.[ewj]ar$') -classpath_regex = re.compile('^\s*Class-Path\s*:', re.M | re.I) +zip_regex = re.compile(r'\.(zip|[ewj]ar)$') +jar_regex = re.compile(r'\.[ewj]ar$') +classpath_regex = re.compile(r'^\s*Class-Path\s*:', re.M | re.I) want_indexed_jars = Config.getOption('UseIndexedJars', True) @@ -37,7 +37,7 @@ class ZipCheck(AbstractCheck.AbstractCheck): if zip_regex.search(fname) and os.path.exists(path) and \ stat.S_ISREG(os.lstat(path)[stat.ST_MODE]) and \ zipfile.is_zipfile(path): - z = None + z = None # TODO ZipFile is context manager in 2.7+ try: z = zipfile.ZipFile(path, 'r') badcrc = z.testzip() @@ -103,5 +103,3 @@ in some situations.''', ) # ZipCheck.py ends here - -# ex: ts=4 sw=4 et diff --git a/config b/config index 17c9941..a56fb4a 100644 --- a/config +++ b/config @@ -162,6 +162,11 @@ from Config import * # Type: boolean, default: autodetected from environment #setOption("UseUTF8", True) +# Whether debug sources are expected to be in separate packages from +# -debuginfo, typically -debugsource. +# Type: boolean, default: False +#setOption("UseDebugSource", False) + # Whether %changelog entries should contain a version. # Type: boolean, default: True #setOption("UseVersionInChangelog", True) diff --git a/rpmdiff b/rpmdiff index 5080891..6d91c60 100755 --- a/rpmdiff +++ b/rpmdiff @@ -42,7 +42,8 @@ class Rpmdiff(object): rpm.RPMTAG_PREUN, rpm.RPMTAG_POSTUN, rpm.RPMTAG_PRETRANS, rpm.RPMTAG_POSTTRANS) - PRCO = ('REQUIRES', 'PROVIDES', 'CONFLICTS', 'OBSOLETES') + PRCO = ('REQUIRES', 'PROVIDES', 'CONFLICTS', 'OBSOLETES', + 'RECOMMENDS', 'SUGGESTS', 'ENHANCES', 'SUPPLEMENTS') # {fname : (size, mode, mtime, flags, dev, inode, # nlink, state, vflags, user, group, digest)} @@ -198,7 +199,11 @@ class Rpmdiff(object): # compare Provides, Requires, Conflicts, Obsoletes def __comparePRCOs(self, old, new, name): - oldflags = old[name[:-1] + 'FLAGS'] + try: + oldflags = old[name[:-1] + 'FLAGS'] + except ValueError: + # assume tag not supported, e.g. Recommends with older rpm + return newflags = new[name[:-1] + 'FLAGS'] # fix buggy rpm binding not returning list for single entries if not isinstance(oldflags, list): @@ -288,9 +293,8 @@ def main(): print(textdiff) sys.exit(int(d.differs())) + if __name__ == '__main__': main() # rpmdiff ends here - -# ex: ts=4 sw=4 et diff --git a/rpmdiff.1 b/rpmdiff.1 index c33a3e1..5f1fff7 100644 --- a/rpmdiff.1 +++ b/rpmdiff.1 @@ -1,5 +1,5 @@ .\" -.\" (C) Copyright 2014, Arturo Borrero Gonzalez , +.\" (C) Copyright 2014, Arturo Borrero Gonzalez , .\" .\" %%%LICENSE_START(GPLv2+_DOC_FULL) .\" This is free documentation; you can redistribute it and/or @@ -66,5 +66,5 @@ Originally written by Frédéric Lepied, modified and maintained by numerous contributors since. .br This manual page was written by Arturo Borrero González -, and is free/libre documentation +, and is free/libre documentation (GPL-2+). diff --git a/rpmlint b/rpmlint index 7af8774..d2d7601 100755 --- a/rpmlint +++ b/rpmlint @@ -11,7 +11,6 @@ import getopt import glob -import imp import locale import os import re @@ -19,6 +18,12 @@ import stat import sys import tempfile +try: + import importlib +except: # Python < 2.7 + importlib = None + import imp + # 1 instead of 0 here because we want the script dir to be looked up first, # e.g. for in-place from tarball or SCM checkout sys.path.insert(1, '/usr/share/rpmlint') @@ -71,11 +76,12 @@ def loadCheck(name): loaded = sys.modules.get(name) if loaded: return loaded - (fobj, pathname, description) = imp.find_module(name) - try: - imp.load_module(name, fobj, pathname, description) - finally: - fobj.close() + if importlib: + importlib.import_module(name) + else: + (fobj, pathname, description) = imp.find_module(name) + with fobj: + imp.load_module(name, fobj, pathname, description) ############################################################################# @@ -109,7 +115,7 @@ def main(): if not stdin: continue with Pkg.FakePkg(arg) as pkg: - runSpecChecks(pkg, arg) + runSpecChecks(pkg, None, spec_lines=stdin) specfiles_checked += 1 continue @@ -162,7 +168,7 @@ def main(): for dname in dirs: try: - for path, dirs, files in os.walk(dname): + for path, _, files in os.walk(dname): for fname in files: fname = os.path.abspath(os.path.join(path, fname)) try: @@ -219,7 +225,7 @@ def runChecks(pkg): Pkg.warn('(none): W: unknown check %s, skipping' % name) -def runSpecChecks(pkg, fname): +def runSpecChecks(pkg, fname, spec_lines=None): if verbose: printInfo(pkg, 'checking') @@ -228,10 +234,11 @@ def runSpecChecks(pkg, fname): check = AbstractCheck.AbstractCheck.known_checks.get(name) if check: check.verbose = verbose - check.check_spec(pkg, fname) + check.check_spec(pkg, fname, spec_lines) else: Pkg.warn('(none): W: unknown check %s, skipping' % name) + ############################################################################# # ############################################################################# @@ -371,5 +378,3 @@ if __name__ == '__main__': main() # rpmlint ends here - -# ex: ts=4 sw=4 et diff --git a/rpmlint.bash-completion b/rpmlint.bash-completion index 51ffe88..db1bad6 100644 --- a/rpmlint.bash-completion +++ b/rpmlint.bash-completion @@ -88,4 +88,4 @@ complete -F _rpmdiff -o filenames rpmdiff # Local variables: # mode: shell-script # End: -# ex: ts=4 sw=4 et filetype=sh +# ex: filetype=sh diff --git a/setup.cfg b/setup.cfg index 3c3a26e..cf06723 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ [flake8] -ignore = E122,E501,H101,H105,H201,H301,H404,H405 +ignore = E122,E501,H101,H105,H201,H301,H404,H405,B001 import-order-style = google application-import-names = __isocodes__,__version__,AbstractCheck,AppDataCheck,BinariesCheck,Config,ConfigCheck,DistributionCheck,DocFilesCheck,FHSCheck,FilesCheck,Filter,I18NCheck,InitScriptCheck,LSBCheck,MenuCheck,MenuXDGCheck,NamingPolicyCheck,PamCheck,Pkg,PostCheck,RpmFileCheck,SCLCheck,SignatureCheck,SourceCheck,SpecCheck,TagsCheck,ZipCheck,Testing diff --git a/test.sh b/test.sh index 044bf23..ad29398 100755 --- a/test.sh +++ b/test.sh @@ -3,6 +3,8 @@ export PYTHONPATH=$(pwd)/tools:$(pwd) export TESTPATH="$(pwd)/test/" : ${PYTHON:=python} ${PYTEST:=py.test} ${FLAKE8:=flake8} +: ${PYTHONWARNINGS:=all} +export PYTHONWARNINGS echo echo "Please ignore the possibly occurring output like this:" @@ -17,15 +19,29 @@ for i in $TESTPATH/test.*.py; do fi done +run_rpmlint="$PYTHON ./rpmlint -C $(pwd)" + echo "Check that rpmlint executes with no unexpected errors" -$PYTHON ./rpmlint -C $(pwd) test/*/*.rpm test/spec/*.spec >/dev/null +echo "...in default locale" +$run_rpmlint test/*/*.rpm test/spec/*.spec >/dev/null +rc=$? +test $rc -eq 0 -o $rc -eq 64 || exit $rc +echo "...in the C locale" +LC_ALL=C $run_rpmlint test/*/*.rpm test/spec/*.spec >/dev/null rc=$? test $rc -eq 0 -o $rc -eq 64 || exit $rc +echo "...with specfile from stdin" +$run_rpmlint - < $TESTPATH/spec/SpecCheck.spec >/dev/null +rc=$? +test $rc -eq 0 || exit $rc echo "$PYTEST tests" $PYTEST -v || exit $? +unset PYTHONWARNINGS + echo "$FLAKE8 tests" +$FLAKE8 --version $FLAKE8 . ./rpmdiff ./rpmlint || exit $? echo "man page tests" diff --git a/test/Dockerfile-centos6 b/test/Dockerfile-centos6 new file mode 100644 index 0000000..5406f5f --- /dev/null +++ b/test/Dockerfile-centos6 @@ -0,0 +1,18 @@ +FROM centos:6 + +RUN yum -y install \ + /usr/bin/cpio \ + /usr/bin/bzip2 \ + /usr/bin/desktop-file-validate \ + /usr/bin/groff \ + /usr/bin/gtbl \ + /usr/bin/make \ + /usr/bin/man \ + /usr/bin/py.test \ + /usr/bin/readelf \ + /usr/bin/xz \ + python-enchant \ + python-magic + +WORKDIR /usr/src/rpmlint +COPY . . diff --git a/test/Dockerfile-fedora26 b/test/Dockerfile-fedora26 new file mode 100644 index 0000000..8725db9 --- /dev/null +++ b/test/Dockerfile-fedora26 @@ -0,0 +1,23 @@ +FROM fedora:26 + +RUN dnf --refresh -y upgrade +RUN dnf -y install \ + /usr/bin/appstream-util \ + /usr/bin/cpio \ + /usr/bin/bzip2 \ + /usr/bin/desktop-file-validate \ + /usr/bin/groff \ + /usr/bin/gtbl \ + /usr/bin/make \ + /usr/bin/man \ + /usr/bin/py.test-3 \ + /usr/bin/flake8-3 \ + /usr/bin/readelf \ + /usr/bin/xz \ + python3-enchant \ + python3-flake8-import-order \ + python3-hacking \ + python3-magic + +WORKDIR /usr/src/rpmlint +COPY . . diff --git a/test/Dockerfile-fedoradev b/test/Dockerfile-fedoradev new file mode 100644 index 0000000..7dc3594 --- /dev/null +++ b/test/Dockerfile-fedoradev @@ -0,0 +1,26 @@ +FROM fedora:rawhide + +# --nogpgcheck: Ignore transient(?) package signature failures +RUN dnf --nogpgcheck --refresh -y upgrade +RUN dnf --nogpgcheck -y install \ + /usr/bin/appstream-util \ + /usr/bin/cpio \ + /usr/bin/bzip2 \ + /usr/bin/desktop-file-validate \ + /usr/bin/groff \ + /usr/bin/gtbl \ + /usr/bin/make \ + /usr/bin/man \ + /usr/bin/readelf \ + /usr/bin/xz \ + python3-enchant \ + python3-magic \ + python3-devel rpm-build +# TODO: Install hacking once it's flake8 3.4+ compatible +RUN pip3 install \ + flake8-bugbear \ + flake8-import-order \ + pytest + +WORKDIR /usr/src/rpmlint +COPY . . diff --git a/test/Dockerfile-ubuntu14 b/test/Dockerfile-ubuntu14 new file mode 100644 index 0000000..23f20b3 --- /dev/null +++ b/test/Dockerfile-ubuntu14 @@ -0,0 +1,18 @@ +FROM ubuntu:14.04 + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ + binutils \ + desktop-file-utils \ + groff-base \ + make \ + man \ + python-enchant \ + python-magic \ + python-pytest \ + python-rpm \ + rpm \ + rpm2cpio \ + xz-utils + +WORKDIR /usr/src/rpmlint +COPY . . diff --git a/test/binary/netmask-debugsource-2.4.3-5.fc27.x86_64.rpm b/test/binary/netmask-debugsource-2.4.3-5.fc27.x86_64.rpm new file mode 100644 index 0000000000000000000000000000000000000000..1eb1ebef4d13ce613acd4b4d487d6aa2837f914c GIT binary patch literal 19038 zcmeIYcU)7;);=D3M~ZZi2vP(Bp(O-Cdhb;P5knFnbdyj-Kq&$O0*VxADj-r6r1vUK zKq-P$K@bp7X$pb`zdM?9-*eBszxSSV-}B$k&1dg5�T)?PDv_MV5lFIk_a009S? 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 QvpUC5{AmY>{