def _check_forbidden_controlchar(self, pkg): """Trigger check forbidden-controlchar-found Check if package contains a forbidden_words or character in tags: Provides, Conflicts, Obsoletes, Supplements, Suggests, Enhances, Recommends and Requires Returns: Output info to STDOUT """ for tagname, items in (('Provides', pkg.provides), ('Conflicts', pkg.conflicts), ('Obsoletes', pkg.obsoletes), ('Supplements', pkg.supplements), ('Suggests', pkg.suggests), ('Enhances', pkg.enhances), ('Recommends', pkg.recommends)): for item in items: dep = Pkg.has_forbidden_controlchars(item) if dep: self.output.add_info('E', pkg, 'forbidden-controlchar-found', '{}: {}'.format(tagname, dep)) value = Pkg.formatRequire(*item) self._unexpanded_macros(pkg, '{} {}'.format(tagname, value), value) # Check if a package contains forbidden-controlchar in Requires: tag. for pkg_token in (pkg.requires): dep = Pkg.has_forbidden_controlchars(pkg_token) if dep: self.output.add_info('E', pkg, 'forbidden-controlchar-found', 'Requires: {}'.format(dep))
def check(self, pkg): packager = pkg[rpm.RPMTAG_PACKAGER] if packager: self._unexpanded_macros(pkg, 'Packager', packager) if self.config.configuration['Packager'] and \ not self.packager_regex.search(packager): self.output.add_info('W', pkg, 'invalid-packager', packager) else: self.output.add_info('E', pkg, 'no-packager-tag') version = pkg[rpm.RPMTAG_VERSION] if version: self._unexpanded_macros(pkg, 'Version', version) res = invalid_version_regex.search(version) if res: self.output.add_info('E', pkg, 'invalid-version', version) else: self.output.add_info('E', pkg, 'no-version-tag') release = pkg[rpm.RPMTAG_RELEASE] if release: self._unexpanded_macros(pkg, 'Release', release) if self.release_ext and not self.extension_regex.search(release): self.output.add_info('W', pkg, 'not-standard-release-extension', release) else: self.output.add_info('E', pkg, 'no-release-tag') epoch = pkg[rpm.RPMTAG_EPOCH] if epoch is None: if self.use_epoch: self.output.add_info('E', pkg, 'no-epoch-tag') else: if epoch > 99: self.output.add_info('W', pkg, 'unreasonable-epoch', epoch) epoch = str(epoch) if self.use_epoch: for tag in ('obsoletes', 'conflicts', 'provides', 'recommends', 'suggests', 'enhances', 'supplements'): for x in (x for x in getattr(pkg, tag)() if x[1] and x[2][0] is None): self.output.add_info('W', pkg, 'no-epoch-in-%s' % tag, Pkg.formatRequire(*x)) name = pkg.name deps = pkg.requires + pkg.prereq devel_depend = False is_devel = FilesCheck.devel_regex.search(name) is_source = pkg.is_source for d in deps: value = Pkg.formatRequire(*d) if self.use_epoch and d[1] and d[2][0] is None and \ not d[0].startswith('rpmlib('): self.output.add_info('W', pkg, 'no-epoch-in-dependency', value) for r in self.invalid_requires: if r.search(d[0]): self.output.add_info('E', pkg, 'invalid-dependency', d[0]) if d[0].startswith('/usr/local/'): self.output.add_info('E', pkg, 'invalid-dependency', d[0]) if is_source: if lib_devel_number_regex.search(d[0]): self.output.add_info('E', pkg, 'invalid-build-requires', d[0]) elif not is_devel: if not devel_depend and FilesCheck.devel_regex.search(d[0]): self.output.add_info('E', pkg, 'devel-dependency', d[0]) devel_depend = True if not d[1]: res = lib_package_regex.search(d[0]) if res and not res.group(1): self.output.add_info('E', pkg, 'explicit-lib-dependency', d[0]) if d[1] == rpm.RPMSENSE_EQUAL and d[2][2] is not None: self.output.add_info('W', pkg, 'requires-on-release', value) self._unexpanded_macros(pkg, 'dependency %s' % (value, ), value) self._unexpanded_macros(pkg, 'Name', name) if not name: self.output.add_info('E', pkg, 'no-name-tag') else: if is_devel and not is_source: base = is_devel.group(1) dep = None has_so = False has_pc = False for fname in pkg.files: if fname.endswith('.so'): has_so = True if pkg_config_regex.match(fname) and fname.endswith('.pc'): has_pc = True if has_so: 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( r'^(lib)?%s(-libs)?[\d_-]*(\(\w+-\d+\))?$' % re.escape(base)) for d in deps: if base_or_libs_re.match(d[0]): dep = d break if not dep: self.output.add_info('W', pkg, 'no-dependency-on', base_or_libs) elif version: exp = (epoch, version, None) sexp = Pkg.versionToString(exp) if not dep[1]: self.output.add_info('W', pkg, 'no-version-dependency-on', base_or_libs, sexp) elif dep[2][:2] != exp[:2]: self.output.add_info( 'W', pkg, 'incoherent-version-dependency-on', base_or_libs, Pkg.versionToString( (dep[2][0], dep[2][1], None)), sexp) res = devel_number_regex.search(name) if not res: self.output.add_info('W', pkg, 'no-major-in-name', name) else: if res.group(3): prov = res.group(1) + res.group(2) + '-devel' else: prov = res.group(1) + '-devel' if prov not in (x[0] for x in pkg.provides): self.output.add_info('W', pkg, 'no-provides', prov) if has_pc: found_pkg_config_dep = False for p in (x[0] for x in pkg.provides): if p.startswith('pkgconfig('): found_pkg_config_dep = True break if not found_pkg_config_dep: self.output.add_info('E', pkg, 'no-pkg-config-provides') # List of words to ignore in spell check ignored_words = set() for pf in pkg.files: ignored_words.update(pf.split('/')) ignored_words.update((x[0] for x in pkg.provides)) ignored_words.update((x[0] for x in pkg.requires)) ignored_words.update((x[0] for x in pkg.conflicts)) ignored_words.update((x[0] for x in pkg.obsoletes)) langs = pkg[rpm.RPMTAG_HEADERI18NTABLE] summary = byte_to_string(pkg[rpm.RPMTAG_SUMMARY]) if summary: if not langs: self._unexpanded_macros(pkg, 'Summary', summary) else: for lang in langs: self.check_summary(pkg, lang, ignored_words) else: self.output.add_info('E', pkg, 'no-summary-tag') description = byte_to_string(pkg[rpm.RPMTAG_DESCRIPTION]) if description: if not langs: self._unexpanded_macros(pkg, '%description', description) else: for lang in langs: self.check_description(pkg, lang, ignored_words) if len(description) < len(pkg[rpm.RPMTAG_SUMMARY]): self.output.add_info('W', pkg, 'description-shorter-than-summary') else: self.output.add_info('E', pkg, 'no-description-tag') group = pkg[rpm.RPMTAG_GROUP] self._unexpanded_macros(pkg, 'Group', group) if not group: self.output.add_info('E', pkg, 'no-group-tag') elif pkg.name.endswith( '-devel') and not group.startswith('Development/'): self.output.add_info('W', pkg, 'devel-package-with-non-devel-group', group) elif self.valid_groups and group not in self.valid_groups: self.output.add_info('W', pkg, 'non-standard-group', group) buildhost = pkg[rpm.RPMTAG_BUILDHOST] self._unexpanded_macros(pkg, 'BuildHost', buildhost) if not buildhost: self.output.add_info('E', pkg, 'no-buildhost-tag') elif self.config.configuration['ValidBuildHost'] and \ not self.valid_buildhost_regex.search(buildhost): self.output.add_info('W', pkg, 'invalid-buildhost', buildhost) changelog = pkg[rpm.RPMTAG_CHANGELOGNAME] if not changelog: self.output.add_info('E', pkg, 'no-changelogname-tag') else: clt = pkg[rpm.RPMTAG_CHANGELOGTEXT] if self.use_version_in_changelog: ret = changelog_version_regex.search( byte_to_string(changelog[0])) if not ret and clt: # we also allow the version specified as the first # thing on the first line of the text ret = changelog_text_version_regex.search( byte_to_string(clt[0])) if not ret: self.output.add_info('W', pkg, 'no-version-in-last-changelog') elif version and release: srpm = pkg[rpm.RPMTAG_SOURCERPM] or '' # only check when source name correspond to name if srpm[0:-8] == '%s-%s-%s' % (name, version, release): expected = [version + '-' + release] if epoch is not None: # regardless of use_epoch expected[0] = str(epoch) + ':' + expected[0] # Allow EVR in changelog without release extension, # the extension is often a macro or otherwise dynamic. if self.release_ext: expected.append( self.extension_regex.sub('', expected[0])) if ret.group(1) not in expected: if len(expected) == 1: expected = expected[0] self.output.add_info( 'W', pkg, 'incoherent-version-in-changelog', ret.group(1), expected) if clt: changelog = changelog + clt for s in changelog: if not Pkg.is_utf8_bytestr(s): self.output.add_info('E', pkg, 'tag-not-utf8', '%changelog') break e = Pkg.has_forbidden_controlchars(s) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', '%%changelog : %s' % e) break clt = pkg[rpm.RPMTAG_CHANGELOGTIME][0] if clt: clt -= clt % (24 * 3600) # roll back to 00:00:00, see #246 if clt < oldest_changelog_timestamp: self.output.add_info( 'W', pkg, 'changelog-time-overflow', time.strftime('%Y-%m-%d', time.gmtime(clt))) elif clt > time.time(): self.output.add_info( 'E', pkg, 'changelog-time-in-future', time.strftime('%Y-%m-%d', time.gmtime(clt))) def split_license(text): return (x.strip() for x in (l for l in license_regex.split(text) if l)) def split_license_exception(text): x, y = license_exception_regex.split(text)[1:3] or (text, '') return x.strip(), y.strip() rpm_license = pkg[rpm.RPMTAG_LICENSE] if not rpm_license: self.output.add_info('E', pkg, 'no-license') else: valid_license = True if rpm_license not in self.valid_licenses: license_string = rpm_license l1, lexception = split_license_exception(rpm_license) # SPDX allows "<license> WITH <license-exception>" if lexception: license_string = l1 if lexception not in self.valid_license_exceptions: self.output.add_info('W', pkg, 'invalid-license-exception', lexception) valid_license = False for l1 in split_license(license_string): if l1 in self.valid_licenses: continue for l2 in split_license(l1): if l2 not in self.valid_licenses: self.output.add_info('W', pkg, 'invalid-license', l2) valid_license = False if not valid_license: self._unexpanded_macros(pkg, 'License', rpm_license) for tag in ('URL', 'DistURL', 'BugURL'): if hasattr(rpm, 'RPMTAG_%s' % tag.upper()): url = byte_to_string(pkg[getattr(rpm, 'RPMTAG_%s' % tag.upper())]) self._unexpanded_macros(pkg, tag, url, is_url=True) if url: (scheme, netloc) = urlparse(url)[0:2] if not scheme or not netloc or '.' not in netloc or \ scheme not in ('http', 'https', 'ftp') or \ (self.config.configuration['InvalidURL'] and self.invalid_url_regex.search(url)): self.output.add_info('W', pkg, 'invalid-url', tag, url) elif tag == 'URL': self.output.add_info('W', pkg, 'no-url-tag') obs_names = [x[0] for x in pkg.obsoletes] prov_names = [x[0] for x in pkg.provides] for o in (x for x in obs_names if x not in prov_names): self.output.add_info('W', pkg, 'obsolete-not-provided', o) for o in pkg.obsoletes: value = Pkg.formatRequire(*o) self._unexpanded_macros(pkg, 'Obsoletes %s' % (value, ), value) # TODO: should take versions, <, <=, =, >=, > into account here # https://bugzilla.redhat.com/460872 useless_provides = set() for p in prov_names: if (prov_names.count(p) != 1 and not p.startswith('debuginfo(') and p not in useless_provides): useless_provides.add(p) for p in sorted(useless_provides): self.output.add_info('E', pkg, 'useless-provides', p) for tagname, items in (('Provides', pkg.provides), ('Conflicts', pkg.conflicts), ('Obsoletes', pkg.obsoletes), ('Supplements', pkg.supplements), ('Suggests', pkg.suggests), ('Enhances', pkg.enhances), ('Recommends', pkg.recommends)): for p in items: e = Pkg.has_forbidden_controlchars(p) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', '%s: %s' % (tagname, e)) value = Pkg.formatRequire(*p) self._unexpanded_macros(pkg, '%s %s' % (tagname, value), value) for p in (pkg.requires): e = Pkg.has_forbidden_controlchars(p) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', 'Requires: %s' % e) obss = pkg.obsoletes if obss: provs = pkg.provides for prov in provs: for obs in obss: if Pkg.rangeCompare(obs, prov): self.output.add_info( 'W', pkg, 'self-obsoletion', '%s obsoletes %s' % (Pkg.formatRequire(*obs), Pkg.formatRequire(*prov))) expfmt = rpm.expandMacro('%{_build_name_fmt}') if pkg.is_source: # _build_name_fmt often (always?) ends up not outputting src/nosrc # as arch for source packages, do it ourselves expfmt = re.sub(r'(?i)%\{?ARCH\b\}?', pkg.arch, expfmt) expected = pkg.header.sprintf(expfmt).split('/')[-1] basename = Path(pkg.filename).parent if basename != expected: self.output.add_info('W', pkg, 'non-coherent-filename', basename, expected) for tag in ('Distribution', 'DistTag', 'ExcludeArch', 'ExcludeOS', 'Vendor'): if hasattr(rpm, 'RPMTAG_%s' % tag.upper()): res = byte_to_string(pkg[getattr(rpm, 'RPMTAG_%s' % tag.upper())]) self._unexpanded_macros(pkg, tag, res)
def check_spec(self, pkg): self._spec_file = pkg.name spec_only = isinstance(pkg, Pkg.FakePkg) spec_lines = Pkg.readlines(self._spec_file) patches = {} applied_patches = [] applied_patches_ifarch = [] patches_auto_applied = False source_dir = False buildroot = False configure_linenum = None configure_cmdline = '' mklibname = False is_lib_pkg = False if_depth = 0 ifarch_depth = -1 current_section = 'package' buildroot_clean = {'clean': False, 'install': False} depscript_override = False depgen_disabled = False patch_fuzz_override = False indent_spaces = 0 indent_tabs = 0 section = {} # None == main package current_package = None package_noarch = {} if self._spec_file: if not Pkg.is_utf8(self._spec_file): self.output.add_info('E', pkg, 'non-utf8-spec-file', self._spec_name or self._spec_file) # gather info from spec lines pkg.current_linenum = 0 nbsp = UNICODE_NBSP for line in spec_lines: pkg.current_linenum += 1 char = line.find(nbsp) if char != -1: self.output.add_info('W', pkg, 'non-break-space', 'line %s, char %d' % (pkg.current_linenum, char)) section_marker = False for sec, regex in section_regexs.items(): res = regex.search(line) if res: current_section = sec section_marker = True section[sec] = section.get(sec, 0) + 1 if sec in ('package', 'files'): rest = filelist_regex.sub('', line[res.end() - 1:]) res = pkgname_regex.search(rest) if res: current_package = res.group(1) else: current_package = None break if section_marker: if not is_lib_pkg and lib_package_regex.search(line): is_lib_pkg = True continue if (current_section in Pkg.RPM_SCRIPTLETS + ('prep', 'build') and contains_buildroot(line)): self.output.add_info('W', pkg, 'rpm-buildroot-usage', '%' + current_section, line[:-1].strip()) if make_check_regex.search(line) and current_section not in \ ('check', 'changelog', 'package', 'description'): self.output.add_info('W', pkg, 'make-check-outside-check-section', line[:-1]) if current_section in buildroot_clean and \ not buildroot_clean[current_section] and \ contains_buildroot(line) and rm_regex.search(line): buildroot_clean[current_section] = True if ifarch_regex.search(line): if_depth = if_depth + 1 ifarch_depth = if_depth if if_regex.search(line): if_depth = if_depth + 1 if setup_regex.match(line): if not setup_q_regex.search(line): # Don't warn if there's a -T without -a or -b if setup_t_regex.search(line): if setup_ab_regex.search(line): self.output.add_info('W', pkg, 'setup-not-quiet') else: self.output.add_info('W', pkg, 'setup-not-quiet') if current_section != 'prep': self.output.add_info('W', pkg, 'setup-not-in-prep') elif autopatch_regex.search(line): patches_auto_applied = True if current_section != 'prep': self.output.add_info('W', pkg, '%autopatch-not-in-prep') else: res = autosetup_regex.search(line) if res: if not autosetup_n_regex.search(res.group(1)): patches_auto_applied = True if current_section != 'prep': self.output.add_info('W', pkg, '%autosetup-not-in-prep') if endif_regex.search(line): if ifarch_depth == if_depth: ifarch_depth = -1 if_depth = if_depth - 1 res = applied_patch_regex.search(line) if res: pnum = res.group(1) or 0 for tmp in applied_patch_p_regex.findall(line) or [pnum]: pnum = int(tmp) applied_patches.append(pnum) if ifarch_depth > 0: applied_patches_ifarch.append(pnum) else: res = applied_patch_pipe_regex.search(line) if res: pnum = int(res.group(1)) 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: source_dir = True self.output.add_info('E', pkg, 'use-of-RPM_SOURCE_DIR') if configure_linenum: if configure_cmdline[-1] == '\\': configure_cmdline = configure_cmdline[:-1] + line.strip() else: res = configure_libdir_spec_regex.search(configure_cmdline) if not res: # Hack to get the correct (start of ./configure) line # number displayed: real_linenum = pkg.current_linenum pkg.current_linenum = configure_linenum self.output.add_info('W', pkg, 'configure-without-libdir-spec') pkg.current_linenum = real_linenum elif res.group(1): res = re.match(hardcoded_library_paths, res.group(1)) if res: self.output.add_info('E', pkg, 'hardcoded-library-path', res.group(1), 'in configure options') configure_linenum = None hashPos = line.find('#') if current_section != 'changelog': cfgPos = line.find('./configure') if cfgPos != -1 and (hashPos == -1 or hashPos > cfgPos): # store line where it started configure_linenum = pkg.current_linenum configure_cmdline = line.strip() res = hardcoded_library_path_regex.search(line) if current_section != 'changelog' and res and not \ (biarch_package_regex.match(pkg.name) or self.hardcoded_lib_path_exceptions_regex.search( res.group(1).lstrip())): self.output.add_info('E', pkg, 'hardcoded-library-path', 'in', res.group(1).lstrip()) if '%mklibname' in line: mklibname = True if current_section == 'package': # Would be cleaner to get sources and patches from the # specfile parsed in Python (see below), but we want to # catch %ifarch'd etc ones as well, and also catch these when # the specfile is not parseable. res = patch_regex.search(line) if res: pnum = int(res.group(1) or 0) patches[pnum] = res.group(2) res = obsolete_tags_regex.search(line) if res: self.output.add_info('W', pkg, 'obsolete-tag', res.group(1)) res = buildroot_regex.search(line) if res: buildroot = True if res.group(1).startswith('/'): self.output.add_info('W', pkg, 'hardcoded-path-in-buildroot-tag', res.group(1)) res = buildarch_regex.search(line) if res: if res.group(1) != 'noarch': self.output.add_info('E', pkg, 'buildarch-instead-of-exclusivearch-tag', res.group(1)) else: package_noarch[current_package] = True res = packager_regex.search(line) if res: self.output.add_info('W', pkg, 'hardcoded-packager-tag', res.group(1)) res = prefix_regex.search(line) if res: if not res.group(1).startswith('%'): self.output.add_info('W', pkg, 'hardcoded-prefix-tag', res.group(1)) res = prereq_regex.search(line) if res: self.output.add_info('E', pkg, 'prereq-use', res.group(2)) res = buildprereq_regex.search(line) if res: self.output.add_info('E', pkg, 'buildprereq-use', res.group(1)) if scriptlet_requires_regex.search(line): self.output.add_info('E', pkg, 'broken-syntax-in-scriptlet-requires', line.strip()) res = requires_regex.search(line) if res: reqs = Pkg.parse_deps(res.group(1)) e = Pkg.has_forbidden_controlchars(reqs) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', 'Requires: %s' % e) for req in unversioned(reqs): if compop_regex.search(req): self.output.add_info('W', pkg, 'comparison-operator-in-deptoken', req) res = provides_regex.search(line) if res: provs = Pkg.parse_deps(res.group(1)) e = Pkg.has_forbidden_controlchars(provs) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', 'Provides: %s' % e) for prov in unversioned(provs): if not prov.startswith('/'): self.output.add_info('W', pkg, 'unversioned-explicit-provides', prov) if compop_regex.search(prov): self.output.add_info('W', pkg, 'comparison-operator-in-deptoken', prov) res = obsoletes_regex.search(line) if res: obses = Pkg.parse_deps(res.group(1)) e = Pkg.has_forbidden_controlchars(obses) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', 'Obsoletes: %s' % e) for obs in unversioned(obses): if not obs.startswith('/'): self.output.add_info('W', pkg, 'unversioned-explicit-obsoletes', obs) if compop_regex.search(obs): self.output.add_info('W', pkg, 'comparison-operator-in-deptoken', obs) res = conflicts_regex.search(line) if res: confs = Pkg.parse_deps(res.group(1)) e = Pkg.has_forbidden_controlchars(confs) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', 'Conflicts: %s' % e) for conf in unversioned(confs): if compop_regex.search(conf): self.output.add_info('W', pkg, 'comparison-operator-in-deptoken', conf) if current_section == 'changelog': e = Pkg.has_forbidden_controlchars(line) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', '%%changelog: %s' % e) for match in self.macro_regex.findall(line): res = re.match('%+', match) if len(res.group(0)) % 2: self.output.add_info('W', pkg, 'macro-in-%changelog', match) else: if not depscript_override: depscript_override = \ depscript_override_regex.search(line) is not None if not depgen_disabled: depgen_disabled = \ depgen_disable_regex.search(line) is not None if not patch_fuzz_override: patch_fuzz_override = \ patch_fuzz_override_regex.search(line) is not None if current_section == 'files': # TODO: check scriptlets for these too? if package_noarch.get(current_package) or \ (current_package not in package_noarch and package_noarch.get(None)): res = libdir_regex.search(line) if res: pkgname = current_package if pkgname is None: pkgname = '(main package)' self.output.add_info('W', pkg, 'libdir-macro-in-noarch-package', pkgname, line.rstrip()) if not indent_tabs and '\t' in line: indent_tabs = pkg.current_linenum if not indent_spaces and indent_spaces_regex.search(line): indent_spaces = pkg.current_linenum # Check if egrep or fgrep is used if current_section not in \ ('package', 'changelog', 'description', 'files'): greps = deprecated_grep_regex.findall(line) if greps: self.output.add_info('W', pkg, 'deprecated-grep', greps) # If not checking spec file only, we're checking one inside a # SRPM -> skip this check to avoid duplicate warnings (#167) if spec_only and self.valid_groups and \ line.lower().startswith('group:'): group = line[6:].strip() if group not in self.valid_groups: self.output.add_info('W', pkg, 'non-standard-group', group) # Test if there are macros in comments if hashPos != -1 and \ (hashPos == 0 or line[hashPos - 1] in (' ', '\t')): for match in self.macro_regex.findall( line[hashPos + 1:]): res = re.match('%+', match) if len(res.group(0)) % 2: self.output.add_info('W', pkg, 'macro-in-comment', match) # Last line read is not useful after this point pkg.current_linenum = None for sect in (x for x in buildroot_clean if not buildroot_clean[x]): self.output.add_info('W', pkg, 'no-cleaning-of-buildroot', '%' + sect) if not buildroot: self.output.add_info('W', pkg, 'no-buildroot-tag') for sec in ('prep', 'build', 'install', 'clean'): if not section.get(sec): self.output.add_info('W', pkg, 'no-%%%s-section' % sec) for sec in ('changelog',): # prep, build, install, clean, check prevented by rpmbuild 4.4 if section.get(sec, 0) > 1: self.output.add_info('W', pkg, 'more-than-one-%%%s-section' % sec) if is_lib_pkg and not mklibname: self.output.add_info('E', pkg, 'lib-package-without-%mklibname') if depscript_override and not depgen_disabled: self.output.add_info('W', pkg, 'depscript-without-disabling-depgen') if patch_fuzz_override: self.output.add_info('W', pkg, 'patch-fuzz-is-changed') if indent_spaces and indent_tabs: pkg.current_linenum = max(indent_spaces, indent_tabs) self.output.add_info('W', pkg, 'mixed-use-of-spaces-and-tabs', '(spaces: line %d, tab: line %d)' % (indent_spaces, indent_tabs)) pkg.current_linenum = None # process gathered info if not patches_auto_applied: for pnum, pfile in patches.items(): if pnum in applied_patches_ifarch: self.output.add_info('W', pkg, '%ifarch-applied-patch', 'Patch%d:' % pnum, pfile) if pnum not in applied_patches: self.output.add_info('W', pkg, 'patch-not-applied', 'Patch%d:' % pnum, pfile) # Rest of the checks require a real spec file if not self._spec_file: return # We'd like to parse the specfile only once using python bindings, # 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 outcmd = subprocess.run( ('rpm', '-q', '--qf=', '-D', '_sourcedir %s' % Path(self._spec_file).parent, '--specfile', self._spec_file), stdout=subprocess.PIPE) text = outcmd.stdout.decode() if text.endswith('\n'): text = text[:-1] parse_error = False for line in text.splitlines(): # No such file or dir hack: https://bugzilla.redhat.com/487855 if 'No such file or directory' not in line: parse_error = True self.output.add_info('E', pkg, 'specfile-error', line) if not parse_error: # grab sources and patches from parsed spec object to get # them with macros expanded for URL checking spec_obj = None rpm.addMacro('_sourcedir', pkg.dirName()) try: ts = rpm.TransactionSet() spec_obj = ts.parseSpec(str(self._spec_file)) except (ValueError, rpm.error): # errors logged above already pass rpm.delMacro('_sourcedir') if spec_obj: try: # rpm < 4.8.0 sources = spec_obj.sources() except TypeError: # rpm >= 4.8.0 sources = spec_obj.sources for src in sources: (url, num, flags) = src (scheme, netloc) = urlparse(url)[0:2] if flags & 1: # rpmspec.h, rpm.org ticket #123 srctype = 'Source' else: srctype = 'Patch' tag = '%s%s' % (srctype, num) if scheme and netloc: continue elif srctype == 'Source' and tarball_regex.search(url): self.output.add_info('W', pkg, 'invalid-url', '%s:' % tag, url)
def check_spec(self, pkg): """Find specfile in specified path and run spec file related checks.""" self._spec_file = pkg.name spec_only = isinstance(pkg, Pkg.FakePkg) spec_lines = readlines(self._spec_file) patches = {} applied_patches = [] applied_patches_ifarch = [] patches_auto_applied = False source_dir = False buildroot = False configure_linenum = None configure_cmdline = '' mklibname = False is_lib_pkg = False if_depth = 0 ifarch_depth = -1 current_section = 'package' buildroot_clean = {'clean': False, 'install': False} depscript_override = False depgen_disabled = False patch_fuzz_override = False indent_spaces = 0 indent_tabs = 0 section = {} # None == main package current_package = None package_noarch = {} # method call self._check_non_utf8_spec_file(pkg) # gather info from spec lines pkg.current_linenum = 0 nbsp = UNICODE_NBSP # Analyse specfile line by line to check for (E)rrors or (W)arnings for line in spec_lines: pkg.current_linenum += 1 char = line.find(nbsp) if char != -1: self.output.add_info( 'W', pkg, 'non-break-space', 'line %s, char %d' % (pkg.current_linenum, char)) section_marker = False for sec, regex in section_regexs.items(): res = regex.search(line) if res: current_section = sec section_marker = True section[sec] = section.get(sec, 0) + 1 if sec in ('package', 'files'): rest = filelist_regex.sub('', line[res.end() - 1:]) res = pkgname_regex.search(rest) if res: current_package = res.group(1) else: current_package = None break if section_marker: if not is_lib_pkg and lib_package_regex.search(line): is_lib_pkg = True continue if (current_section in Pkg.RPM_SCRIPTLETS + ('prep', 'build') and contains_buildroot(line)): self.output.add_info('W', pkg, 'rpm-buildroot-usage', '%' + current_section, line[:-1].strip()) if make_check_regex.search(line) and current_section not in \ ('check', 'changelog', 'package', 'description'): self.output.add_info('W', pkg, 'make-check-outside-check-section', line[:-1]) if current_section in buildroot_clean and \ not buildroot_clean[current_section] and \ contains_buildroot(line) and rm_regex.search(line): buildroot_clean[current_section] = True if ifarch_regex.search(line): if_depth = if_depth + 1 ifarch_depth = if_depth if if_regex.search(line): if_depth = if_depth + 1 if setup_regex.match(line): if not setup_q_regex.search(line): # Don't warn if there's a -T without -a or -b if setup_t_regex.search(line): if setup_ab_regex.search(line): self.output.add_info('W', pkg, 'setup-not-quiet') else: self.output.add_info('W', pkg, 'setup-not-quiet') if current_section != 'prep': self.output.add_info('W', pkg, 'setup-not-in-prep') elif autopatch_regex.search(line): patches_auto_applied = True if current_section != 'prep': self.output.add_info('W', pkg, '%autopatch-not-in-prep') else: res = autosetup_regex.search(line) if res: if not autosetup_n_regex.search(res.group(1)): patches_auto_applied = True if current_section != 'prep': self.output.add_info('W', pkg, '%autosetup-not-in-prep') if endif_regex.search(line): if ifarch_depth == if_depth: ifarch_depth = -1 if_depth = if_depth - 1 res = applied_patch_regex.search(line) if res: pnum = res.group(1) or 0 for tmp in applied_patch_p_regex.findall(line) or [pnum]: pnum = int(tmp) applied_patches.append(pnum) if ifarch_depth > 0: applied_patches_ifarch.append(pnum) else: res = applied_patch_pipe_regex.search(line) if res: pnum = int(res.group(1)) 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: source_dir = True self.output.add_info('E', pkg, 'use-of-RPM_SOURCE_DIR') if configure_linenum: if configure_cmdline[-1] == '\\': configure_cmdline = configure_cmdline[:-1] + line.strip() else: res = configure_libdir_spec_regex.search(configure_cmdline) if not res: # Hack to get the correct (start of ./configure) line # number displayed: real_linenum = pkg.current_linenum pkg.current_linenum = configure_linenum self.output.add_info('W', pkg, 'configure-without-libdir-spec') pkg.current_linenum = real_linenum elif res.group(1): res = re.match(hardcoded_library_paths, res.group(1)) if res: self.output.add_info('E', pkg, 'hardcoded-library-path', res.group(1), 'in configure options') configure_linenum = None hash_pos = line.find('#') if current_section != 'changelog': cfg_pos = line.find('./configure') if cfg_pos != -1 and (hash_pos == -1 or hash_pos > cfg_pos): # store line where it started configure_linenum = pkg.current_linenum configure_cmdline = line.strip() res = hardcoded_library_path_regex.search(line) if current_section != 'changelog' and res and not \ (biarch_package_regex.match(pkg.name) or self.hardcoded_lib_path_exceptions_regex.search( res.group(1).lstrip())): self.output.add_info('E', pkg, 'hardcoded-library-path', 'in', res.group(1).lstrip()) if '%mklibname' in line: mklibname = True if current_section == 'package': # Would be cleaner to get sources and patches from the # specfile parsed in Python (see below), but we want to # catch %ifarch'd etc ones as well, and also catch these when # the specfile is not parseable. res = patch_regex.search(line) if res: pnum = int(res.group(1) or 0) patches[pnum] = res.group(2) res = obsolete_tags_regex.search(line) if res: self.output.add_info('W', pkg, 'obsolete-tag', res.group(1)) res = buildroot_regex.search(line) if res: buildroot = True if res.group(1).startswith('/'): self.output.add_info( 'W', pkg, 'hardcoded-path-in-buildroot-tag', res.group(1)) res = buildarch_regex.search(line) if res: if res.group(1) != 'noarch': self.output.add_info( 'E', pkg, 'buildarch-instead-of-exclusivearch-tag', res.group(1)) else: package_noarch[current_package] = True res = packager_regex.search(line) if res: self.output.add_info('W', pkg, 'hardcoded-packager-tag', res.group(1)) res = prefix_regex.search(line) if res: if not res.group(1).startswith('%'): self.output.add_info('W', pkg, 'hardcoded-prefix-tag', res.group(1)) res = prereq_regex.search(line) if res: self.output.add_info('E', pkg, 'prereq-use', res.group(2)) res = buildprereq_regex.search(line) if res: self.output.add_info('E', pkg, 'buildprereq-use', res.group(1)) res = requires_regex.search(line) if res: reqs = Pkg.parse_deps(res.group(1)) deptoken = Pkg.has_forbidden_controlchars(reqs) if deptoken: self.output.add_info('E', pkg, 'forbidden-controlchar-found', f'Requires: {deptoken}') for req in unversioned(reqs): if compop_regex.search(req): self.output.add_info( 'W', pkg, 'comparison-operator-in-deptoken', req) res = provides_regex.search(line) if res: provs = Pkg.parse_deps(res.group(1)) deptoken = Pkg.has_forbidden_controlchars(provs) if deptoken: self.output.add_info('E', pkg, 'forbidden-controlchar-found', f'Provides: {deptoken}') for prov in unversioned(provs): if not prov.startswith('/'): self.output.add_info( 'W', pkg, 'unversioned-explicit-provides', prov) if compop_regex.search(prov): self.output.add_info( 'W', pkg, 'comparison-operator-in-deptoken', prov) res = obsoletes_regex.search(line) if res: obses = Pkg.parse_deps(res.group(1)) deptoken = Pkg.has_forbidden_controlchars(obses) if deptoken: self.output.add_info('E', pkg, 'forbidden-controlchar-found', f'Obsoletes: {deptoken}') for obs in unversioned(obses): if not obs.startswith('/'): self.output.add_info( 'W', pkg, 'unversioned-explicit-obsoletes', obs) if compop_regex.search(obs): self.output.add_info( 'W', pkg, 'comparison-operator-in-deptoken', obs) res = conflicts_regex.search(line) if res: confs = Pkg.parse_deps(res.group(1)) deptoken = Pkg.has_forbidden_controlchars(confs) if deptoken: self.output.add_info('E', pkg, 'forbidden-controlchar-found', f'Conflicts: {deptoken}') for conf in unversioned(confs): if compop_regex.search(conf): self.output.add_info( 'W', pkg, 'comparison-operator-in-deptoken', conf) if current_section == 'changelog': deptoken = Pkg.has_forbidden_controlchars(line) if deptoken: self.output.add_info('E', pkg, 'forbidden-controlchar-found', '%%changelog: %s' % deptoken) for match in self.macro_regex.findall(line): res = re.match('%+', match) if len(res.group(0)) % 2: self.output.add_info('W', pkg, 'macro-in-%changelog', match) else: if not depscript_override: depscript_override = \ depscript_override_regex.search(line) is not None if not depgen_disabled: depgen_disabled = \ depgen_disable_regex.search(line) is not None if not patch_fuzz_override: patch_fuzz_override = \ patch_fuzz_override_regex.search(line) is not None if current_section == 'files': # TODO: check scriptlets for these too? if package_noarch.get(current_package) or \ (current_package not in package_noarch and package_noarch.get(None)): res = libdir_regex.search(line) if res: pkgname = current_package if pkgname is None: pkgname = '(main package)' self.output.add_info('W', pkg, 'libdir-macro-in-noarch-package', pkgname, line.rstrip()) if not indent_tabs and '\t' in line: indent_tabs = pkg.current_linenum if not indent_spaces and indent_spaces_regex.search(line): indent_spaces = pkg.current_linenum # Check if egrep or fgrep is used if current_section not in \ ('package', 'changelog', 'description', 'files'): greps = deprecated_grep_regex.findall(line) if greps: self.output.add_info('W', pkg, 'deprecated-grep', greps) # If not checking spec file only, we're checking one inside a # SRPM -> skip this check to avoid duplicate warnings (#167) if spec_only and self.valid_groups and \ line.lower().startswith('group:'): group = line[6:].strip() if group not in self.valid_groups: self.output.add_info('W', pkg, 'non-standard-group', group) # Test if there are macros in comments if hash_pos != -1 and \ (hash_pos == 0 or line[hash_pos - 1] in (' ', '\t')): for match in self.macro_regex.findall(line[hash_pos + 1:]): res = re.match('%+', match) if len(res.group(0)) % 2: self.output.add_info('W', pkg, 'macro-in-comment', match) # Last line read is not useful after this point pkg.current_linenum = None # Run checks for whole package self._check_no_cleaning_of_buildroot(pkg, buildroot_clean) self._check_no_buildroot_tag(pkg, buildroot) self._check_no_s_section(pkg, section) self._check_more_than_one_changelog_section(pkg, section) self._check_lib_package_without_mklibname(pkg, is_lib_pkg, mklibname) self._check_descript_without_disabling_depgen(pkg, depscript_override, depgen_disabled) self._check_patch_fuzz_is_changed(pkg, patch_fuzz_override) self._check_mixed_use_of_space_and_tabs(pkg, indent_spaces, indent_tabs) self.check_ifarch_and_not_applied_patches(pkg, patches_auto_applied, patches, applied_patches_ifarch, applied_patches) # Checks below require a real spec file if not self._spec_file: return self._check_specfile_error(pkg, subprocess) self._check_invalid_url(pkg, rpm)
def _check_changelog_tag(self, pkg, changelog, version, release, name, epoch): """Trigger multiple check of type *-changelog, *-changelogname-*, changelog-* and forbidden-controlchar Contains all the checks that cause an issue during build of the rpm in the %changelog of the specfile Args: changelog: Find the %changelog in the specfile Returns: Output info to STDOUT """ # Check if a package does not have a %changelog in its spec file if not changelog: self.output.add_info('E', pkg, 'no-changelogname-tag') else: clt = pkg[rpm.RPMTAG_CHANGELOGTEXT] if self.use_version_in_changelog: ret = changelog_version_regex.search( byte_to_string(changelog[0])) if not ret and clt: # we also allow the version specified as the first # thing on the first line of the text ret = changelog_text_version_regex.search( byte_to_string(clt[0])) # Check if a package does not have version in the %changelog in latest version if not ret: self.output.add_info('W', pkg, 'no-version-in-last-changelog') elif version and release: srpm = pkg[rpm.RPMTAG_SOURCERPM] or '' # only check when source name correspond to name if srpm[0:-8] == '%s-%s-%s' % (name, version, release): expected = [version + '-' + release] if epoch is not None: # regardless of use_epoch expected[0] = str(epoch) + ':' + expected[0] # Allow EVR in changelog without release extension, # the extension is often a macro or otherwise dynamic. if self.release_ext: expected.append( self.extension_regex.sub('', expected[0])) # Check if a package does not have a version that is # compatible with epoch:vesrion-release tuple if ret.group(1) not in expected: if len(expected) == 1: expected = expected[0] self.output.add_info( 'W', pkg, 'incoherent-version-in-changelog', ret.group(1), expected) if clt: changelog = changelog + clt for deptoken in changelog: dep = Pkg.has_forbidden_controlchars(deptoken) # Check if a package contains a forbidden character in %changelog if dep: self.output.add_info('E', pkg, 'forbidden-controlchar-found', '%%changelog : %s' % dep) break clt = pkg[rpm.RPMTAG_CHANGELOGTIME][0] if clt: clt -= clt % (24 * 3600) # roll back to 00:00:00, see #246 # Check if a package contains a changelog entry that is suspiciously too far behind if clt < oldest_changelog_timestamp: self.output.add_info( 'W', pkg, 'changelog-time-overflow', time.strftime('%Y-%m-%d', time.gmtime(clt))) # Check if a package contians a entry in %changelog # with timestamp thats in the future of its writing elif clt > time.time(): self.output.add_info( 'E', pkg, 'changelog-time-in-future', time.strftime('%Y-%m-%d', time.gmtime(clt)))