def load_config(self, config=None): """ Load the configuration files and append it to local dictionary. It's stored in self.configuration with the content of already loaded options. """ if config: # just add the new config at the end of the list, someone injected # config file to us for path in config: if path not in self.conf_files and path.exists(): self.conf_files.append(path) cfg = {} for cf in sorted(self.conf_files, key=self._sort_config_files): try: toml_config = toml.load(cf) self._merge_dictionaries(cfg, toml_config, self._is_override_config(cf)) except toml.decoder.TomlDecodeError as terr: print_warning( f'(none): E: fatal error while parsing configuration file {cf}: {terr}' ) sys.exit(4) self.configuration = cfg
def process_diff_args(argv): """ Process the passed arguments and return the result :param argv: passed arguments """ parser = argparse.ArgumentParser( prog='rpmdiff', description='Shows basic differences between two rpm packages') parser.add_argument('old_package', metavar='RPM_ORIG', type=Path, help='the old package') parser.add_argument('new_package', metavar='RPM_NEW', type=Path, help='the new package') parser.add_argument('-V', '--version', action='version', version=__version__, help='show package version and exit') parser.add_argument( '-i', '--ignore', nargs='+', default=None, choices=['S', 'M', '5', 'D', 'N', 'L', 'V', 'U', 'G', 'F', 'T'], help="""file property to ignore when calculating differences. Valid values are: S (size), M (mode), 5 (checksum), D (device), N (inode), L (number of links), V (vflags), U (user), G (group), F (digest), T (time)""") parser.add_argument('-e', '--exclude', metavar='GLOB', nargs='+', default=None, help="""Paths to exclude when showing differences. Takes a glob. When absolute (starting with /) all files in a matching directory are excluded as well. When relative, files matching the pattern anywhere are excluded but not directory contents.""") # print help if there is no argument or less than the 2 mandatory ones if len(argv) < 2: parser.print_help() sys.exit(0) options = parser.parse_args(args=argv) # the rpms must exist for us to do anything if not options.old_package.exists(): print_warning(f"The file '{options.old_package}' does not exist") exit(2) if not options.new_package.exists(): print_warning(f"The file '{options.new_package}' does not exist") exit(2) # convert options to dict options_dict = vars(options) return options_dict
def find_configs(self, config=None): """ Load all the configuration files from XDG_CONFIG_DIRS. User can override and then that is added too. """ # first load up the file that contains defaults self.conf_files.append(self.config_defaults) # Then load up config directories on system for directory in reversed(xdg_config_dirs): confdir = Path(directory) / 'rpmlint' if confdir.is_dir(): # load all configs in the folders confopts = sorted(confdir.glob('*config')) self.conf_files += confopts # As a last item load up the user configuration if config: if config.exists(): # load this only if it really exist self.conf_files.append(config) else: print_warning( '(none): W: error locating user requested configuration: {}' .format(config))
def _extract_rpm(self, dirname, verbose): if not Path(dirname).is_dir(): print_warning('Unable to access dir %s' % dirname) elif dirname == '/': # it is an InstalledPkg pass else: self.__tmpdir = tempfile.TemporaryDirectory( prefix='rpmlint.%s.' % Path(self.filename).name, dir=dirname) dirname = self.__tmpdir.name # TODO: sequence based command invocation # TODO: warn some way if this fails (e.g. rpm2cpio not installed) # BusyBox' cpio does not support '-D' argument and the only safe # usage is doing chdir before invocation. filename = Path(self.filename).resolve() cwd = os.getcwd() os.chdir(dirname) command_str = f'rpm2cpio {quote(str(filename))} | cpio -id ; chmod -R +rX .' stderr = None if verbose else subprocess.DEVNULL subprocess.check_output(command_str, shell=True, env=ENGLISH_ENVIROMENT, stderr=stderr) os.chdir(cwd) self.extracted = True return dirname
def _validate_conf_location(string): """ Help validate configuration location during argument parsing. We accept either one configuration file or a directory (then it processes all *.toml files in this directory). It exits the program if location doesn't exist. Args: string: A string representing configuration path (file or directory). Returns: A list with individual paths for each configuration file found. """ config_paths = [] path = Path(string) # Exit if file or dir doesn't exist if not path.exists(): print_warning( f"File or dir with user specified configuration '{string}' does not exist" ) exit(2) if path.is_dir(): config_paths.extend(path.glob('*.toml')) elif path.is_file(): config_paths.append(path) return config_paths
def find_configs(self, config=None): """ Find and store paths to all config files. It searches for default configuration, files in XDG_CONFIG_DIRS and user defined configuration (argument "config"). All configuration file paths found are then stored in self.conf_files variable. XDG_CONFIG_DIRS contains preference-ordered set of base directories to search for configuration files. Users can override it by their own configuration file (config parameter) and then that is added too. """ # first load up the file that contains defaults self.conf_files.append(self.config_defaults) # Skip auto-loading when running under PYTEST if not os.environ.get('PYTEST_XDIST_TESTRUNUID'): # Then load up config directories on system for directory in reversed(xdg_config_dirs): confdir = Path(directory) / 'rpmlint' if confdir.is_dir(): # load all configs in the folders confopts = sorted(confdir.glob('*toml')) self.conf_files += confopts # As a last item load up the user configuration if config: for path in config: if path.exists(): # load this only if it really exist self.conf_files.append(path) else: print_warning( f'(none): W: error locating user requested configuration: {path}' )
def _init_checker(self, lang='en_US'): """ Initialize a checker of selected language if it is not yet present lang: language to initialize the dictionary """ # C language means English if lang == 'C': lang = 'en_US' # test if we actually have working enchant if not ENCHANT: print_warning( '(none): W: unable to init enchant, spellchecking disabled.') return # there might not be myspell/aspell/etc dicts present broker = Broker() if not broker.dict_exists(lang): print_warning( f'(none): W: unable to load spellchecking dictionary for {lang}.' ) return if lang not in self._enchant_checkers: checker = SpellChecker( lang, filters=[EmailFilter, URLFilter, WikiWordFilter]) self._enchant_checkers[lang] = checker
def run(self): # if we just want to print config, do so and leave if self.options['print_config']: self.print_config() return 0 # just explain the error and abort too if self.options['explain']: self.print_explanation(self.options['explain']) return 0 # if no exclusive option is passed then just loop over all the # arguments that are supposed to be either rpm or spec files self.validate_files(self.options['rpmfile']) print(self.output.print_results(self.output.results)) print('{} packages and {} specfiles checked; {} errors, {} warnings'. format(self.packages_checked, self.specfiles_checked, self.output.printed_messages['E'], self.output.printed_messages['W'])) if self.output.badness_threshold > 0 and self.output.score > self.output.badness_threshold: print_warning( f'(none): E: Badness {self.output.score} exceeeds threshold {self.output.badness_threshold}, aborting.' ) return 66 if self.output.printed_messages['E'] > 0: return 64 return 0
def _load_rpmlintrc(self): """ Load rpmlintrc from argument or load up from folder """ if self.options['rpmlintrc']: self.config.load_rpmlintrc(self.options['rpmlintrc']) else: # load only from the same folder specname.rpmlintrc or specname-rpmlintrc # do this only in a case where there is one folder parameter or one file # to avoid multiple folders handling rpmlintrc = [] if not len(self.options['rpmfile']) == 1: return pkg = self.options['rpmfile'][0] if pkg.is_file(): pkg = pkg.parent rpmlintrc += sorted(pkg.glob('*.rpmlintrc')) rpmlintrc += sorted(pkg.glob('*-rpmlintrc')) if len(rpmlintrc) > 1: # multiple rpmlintrcs are highly undesirable print_warning( 'There are multiple items to be loaded for rpmlintrc, ignoring them: {}.' .format(' '.join(map(str, rpmlintrc)))) elif len(rpmlintrc) == 1: self.options['rpmlintrc'] = rpmlintrc[0] self.config.load_rpmlintrc(rpmlintrc[0])
def _load_rpmlintrc(self): """ Load rpmlintrc from argument or load up from folder """ if self.options['rpmlintrc']: # Right now, we allow loading of just a single file, but the 'opensuse' # branch contains auto-loading mechanism that can eventually load # multiple files. for rcfile in self.options['rpmlintrc']: self.config.load_rpmlintrc(rcfile) else: # load only from the same folder specname.rpmlintrc or specname-rpmlintrc # do this only in a case where there is one folder parameter or one file # to avoid multiple folders handling rpmlintrc = [] if len(self.options['rpmfile']) != 1: return pkg = self.options['rpmfile'][0] if pkg.is_file(): pkg = pkg.parent rpmlintrc += sorted(pkg.glob('*.rpmlintrc')) rpmlintrc += sorted(pkg.glob('*-rpmlintrc')) if len(rpmlintrc) > 1: # multiple rpmlintrcs are highly undesirable print_warning( 'There are multiple items to be loaded for rpmlintrc, ignoring them: {}.' .format(' '.join(map(str, rpmlintrc)))) elif len(rpmlintrc) == 1: self.options['rpmlintrc'] = rpmlintrc[0] self.config.load_rpmlintrc(rpmlintrc[0])
def process_lint_args(argv): """ Process the passed arguments and return the result :param argv: passed arguments """ parser = argparse.ArgumentParser(prog='rpmlint', description='Check for common problems in rpm packages') parser.add_argument('rpmfile', nargs='*', type=Path, help='files to be validated by rpmlint') parser.add_argument('-V', '--version', action='version', version=__version__, help='show package version and exit') parser.add_argument('-c', '--config', type=_validate_conf_location, help='load up additional configuration data from specified path (file or directory with *.toml files') parser.add_argument('-e', '--explain', nargs='+', default='', help='provide detailed explanation for one specific message id') parser.add_argument('-r', '--rpmlintrc', type=Path, help='load up specified rpmlintrc file') parser.add_argument('-v', '--verbose', '--info', action='store_true', help='provide detailed explanations where available') parser.add_argument('-p', '--print-config', action='store_true', help='print the settings that are in effect when using the rpmlint') parser.add_argument('-i', '--installed', nargs='+', default='', help='installed packages to be validated by rpmlint') parser.add_argument('-t', '--time-report', action='store_true', help='print time report for run checks') parser.add_argument('-T', '--profile', action='store_true', help='print cProfile report') lint_modes_parser = parser.add_mutually_exclusive_group() lint_modes_parser.add_argument('-s', '--strict', action='store_true', help='treat all messages as errors') lint_modes_parser.add_argument('-P', '--permissive', action='store_true', help='treat individual errors as non-fatal') # print help if there is no argument if len(argv) < 1: parser.print_help() sys.exit(0) options = parser.parse_args(args=argv) # make sure rpmlintrc exists if options.rpmlintrc: if not options.rpmlintrc.exists(): print_warning(f"User specified rpmlintrc '{options.rpmlintrc}' does not exist") exit(2) # validate all the rpmlfile options to be either file or folder f_path = [] invalid_path = False for item in options.rpmfile: p_path = Path() pattern = None for pos, component in enumerate(item.parts): if ('*' in component) or ('?' in component): pattern = '/'.join(item.parts[pos:]) break p_path = p_path / component p_path = list(p_path.glob(pattern)) if pattern else [p_path] for path in p_path: if not path.exists(): print_warning(f"The file or directory '{path}' does not exist") invalid_path = True f_path += p_path if invalid_path: exit(2) # convert options to dict options_dict = vars(options) # use computed rpmfile options_dict['rpmfile'] = f_path return options_dict
def runChecks(pkg): for name in cfg.configuration['Checks']: check = AbstractCheck.known_checks.get(name) if check: check.verbose = verbose check.check(pkg) else: print_warning('(none): W: unknown check %s, skipping' % name)
def runSpecChecks(pkg, fname, spec_lines=None): for name in cfg.configuration['Checks']: check = AbstractCheck.known_checks.get(name) if check: check.verbose = verbose check.check_spec(pkg, fname, spec_lines) else: print_warning('(none): W: unknown check %s, skipping' % name)
def _load_installed_rpms(self, packages): existing_packages = [] for name in packages: pkg = getInstalledPkgs(name) if pkg: existing_packages.extend(pkg) else: print_warning(f'(none): E: there is no installed rpm "{name}".') return existing_packages
def _load_descriptions(): descriptions = {} descr_folder = Path(__file__).parent / 'descriptions' try: description_files = sorted(descr_folder.glob('*.toml')) descriptions = toml.load(description_files) except toml.decoder.TomlDecodeError as terr: print_warning(f'(none): W: unable to parse description files: {terr}') return descriptions
def test_warnprint(capsys): """ Check we print stuff to stderr """ message = 'I am writing to stderr' helpers.print_warning(message) out, err = capsys.readouterr() assert message not in out assert message in err
def validate_file(self, pname): try: if pname.suffix == '.rpm' or pname.suffix == '.spm': with Pkg(pname, self.config.configuration['ExtractDir']) as pkg: self.run_checks(pkg) elif pname.suffix == '.spec': with FakePkg(pname) as pkg: self.run_spec_checks(pkg) except Exception as e: print_warning(f'(none): E: while reading {pname}: {e}')
def check(self, pkg): retcode, output = pkg.check_signature() # Skip all signature checks if check_signature output is empty if output is None: print_warning(f'No output from check_signature() for ' f'{pkg.filename}. Skipping signature checks.') return self._check_no_signature(pkg, retcode, output) self._check_unknown_key(pkg, retcode, output) self._check_invalid_signature(pkg, retcode, output)
def add_check(self, check): """ Add specified file to be loaded up by checks. Check is just a string file. It used to be possible to specify additional locations for checks but to keep it simple all checks must be part of rpmlint package -> from rpmlint.checks.<CHECKNAME> import * """ # Validate first if it is possible to import the added check if find_spec('.{}'.format(check), package='rpmlint.checks'): self.configuration['Checks'].append(check) else: print_warning( '(none): W: error adding requested check: {}'.format(check))
def grep(self, regex, filename): """Grep regex from a file, return matching line numbers.""" ret = [] lineno = 0 try: with open(os.path.join( self.dirName() or '/', filename.lstrip('/'))) as in_file: for line in in_file: lineno += 1 if regex.search(line): ret.append(str(lineno)) break except Exception as e: print_warning(f'Unable to read {filename}: {e}') return ret
def _extract(self): if not Path(self.dirname).is_dir(): print_warning('Unable to access dir %s' % self.dirname) else: self.__tmpdir = tempfile.TemporaryDirectory( prefix='rpmlint.%s.' % Path(self.filename).name, dir=self.dirname) self.dirname = self.__tmpdir.name # TODO: sequence based command invocation # TODO: warn some way if this fails (e.g. rpm2cpio not installed) command_str = \ 'rpm2cpio %(f)s | cpio -id -D %(d)s ; chmod -R +rX %(d)s' % \ {'f': quote(str(self.filename)), 'd': quote(str(self.dirname))} subprocess.run(command_str, shell=True) self.extracted = True
def check(self, pkg): res = pkg.checkSignature() if not res or res[0] != 0: if res and res[1]: kres = SignatureCheck.unknown_key_regex.search(res[1]) else: kres = None if kres: self.output.add_info('E', pkg, 'unknown-key', kres.group(1)) else: print_warning('Error checking signature of %s: %s' % (pkg.filename, res[1])) else: if not SignatureCheck.pgp_regex.search(res[1]): self.output.add_info('E', pkg, 'no-signature')
def validate_files(self, files): """ Run all the check for passed file list """ if not files: if self.packages_checked == 0: # print warning only if we didn't process even installed files print_warning('There are no files to process nor additional arguments.') print_warning('Nothing to do, aborting.') return # check all elements if they are a folder or a file with proper suffix # and expand everything packages = self._expand_filelist(files) for pkg in packages: self.validate_file(pkg)
def load_config(self, config=None): """ Load the configuration files and append it to local dictionary with the content of already loaded options. """ if config and config not in self.conf_files: # just add the config at the end of the list, someone injected # config file to us if config.exists(): self.conf_files.append(config) try: cfg = toml.load(self.conf_files) except toml.decoder.TomlDecodeError as terr: print_warning(f'(none): W: error parsing configuration files: {terr}') cfg = None self.configuration = cfg
def _extract(self): if not os.path.isdir(self.dirname): print_warning('Unable to access dir %s' % self.dirname) return None else: self.dirname = tempfile.mkdtemp( prefix='rpmlint.%s.' % os.path.basename(self.filename), dir=self.dirname) # TODO: sequence based command invocation # TODO: warn some way if this fails (e.g. rpm2cpio not installed) command_str = \ 'rpm2cpio %(f)s | (cd %(d)s; cpio -id); chmod -R +rX %(d)s' % \ {'f': shquote(self.filename), 'd': shquote(self.dirname)} cmd = getstatusoutput(command_str, shell=True) self.extracted = True return cmd
def validate_file(self, pname, is_last): try: if pname.suffix == '.rpm' or pname.suffix == '.spm': with Pkg(pname, self.config.configuration['ExtractDir'], verbose=self.config.info) as pkg: self.check_duration['rpm2cpio'] += pkg.extraction_time self.run_checks(pkg, is_last) elif pname.suffix == '.spec': with FakePkg(pname) as pkg: self.run_checks(pkg, is_last) except Exception as e: print_warning(f'(none): E: fatal error while reading {pname}: {e}') if self.config.info: raise e else: sys.exit(3)
def load_rpmlintrc(self, rpmlint_file): """ Function to load up existing rpmlintrc files Only setBadness and addFilter are processed """ if not self.configuration: print_warning( '(none): W: loading rpmlint before configuration is not allowed: {}' .format(rpmlint_file)) return with open(rpmlint_file) as f: rpmlintrc_content = f.read() filters = self.re_filter.findall(rpmlintrc_content) self.configuration['Filters'] += filters badness = self.re_badness.findall(rpmlintrc_content) for entry in badness: self.configuration['Scoring'].update({entry[0]: entry[1]})
def _extract(self, dirname, verbose): if not Path(dirname).is_dir(): print_warning('Unable to access dir %s' % dirname) else: dirname = dirname if dirname != '/' else None self.__tmpdir = tempfile.TemporaryDirectory( prefix='rpmlint.%s.' % Path(self.filename).name, dir=dirname ) dirname = self.__tmpdir.name # TODO: sequence based command invocation # TODO: warn some way if this fails (e.g. rpm2cpio not installed) command_str = \ 'rpm2cpio %(f)s | cpio -id -D %(d)s ; chmod -R +rX %(d)s' % \ {'f': quote(str(self.filename)), 'd': quote(dirname)} stderr = None if verbose else subprocess.DEVNULL subprocess.check_output(command_str, shell=True, env=ENGLISH_ENVIROMENT, stderr=stderr) self.extracted = True return dirname
def _load_descriptions(): """ Load rpmlint error/warning description texts from toml files. Detailed description for every rpmlint error/warning is stored in descriptions/<check_name>.toml file. Returns: A dictionary mapping error/warning/info names to their descriptions. """ descriptions = {} descr_folder = Path(__file__).parent / 'descriptions' try: description_files = sorted(descr_folder.glob('*.toml')) descriptions = toml.load(description_files) except toml.decoder.TomlDecodeError as terr: print_warning( f'(none): W: unable to parse description files: {terr}') return descriptions
def load_config(self, config): """ Load the configuration file and append it to local dictionary with the content of already loaded options. """ if config not in self.conf_files: # just add the config for tracking purposes, someone injected # config file to us self.conf_files.append(config) # load and validate initial config val = Validator() configspec = ConfigObj(self.__configspecfilename, _inspec=True) cfg = ConfigObj(config, configspec=configspec) if not cfg.validate(val): print_warning( '(none): W: error parsing configuration file: {}'.format( config)) # load multiline defaults cfg = self._load_defaults(cfg, DEFAULTS) cfg = self._load_defaults(cfg, DICT_DEFAULTS) # convert all list items to real lists cfg = self._convert_known_lists(cfg, self.known_lists_merged) cfg = self._convert_known_lists(cfg, self.known_lists_override, True) # for merging we have duplicate object without filled in defaults result = ConfigObj(config) # conver the result stuff to lists too result = self._convert_known_lists(result, self.known_lists_merged) result = self._convert_known_lists(result, self.known_lists_override, True) # merge the dict on where we are merging lists for i in self.known_lists_merged: if self.configuration: if i in self.configuration and i in result: result[i] = result[i] + self.configuration[i] # Merge stuff in a case we alrady have config if self.configuration: self.configuration.merge(result) else: self.configuration = cfg