def show_unread_news(): """ Shows unread news from archlinux.org """ # load list of already seen news try: os.makedirs(Package.cache_dir, mode=0o700, exist_ok=True) except OSError: logging.error("Creating cache dir {} failed".format(Package.cache_dir)) raise InvalidInput("Creating cache dir {} failed".format( Package.cache_dir)) seen_ids_file = os.path.join(Package.cache_dir, "seen_news_ids") if not os.path.isfile(seen_ids_file): open(seen_ids_file, 'a').close() with open(seen_ids_file, 'r') as seenidsfile: seen_ids: Set[str] = set( [line for line in seenidsfile.read().strip().splitlines()]) # fetch current news and filter unseen news news_to_show = list( reversed( list( filter( lambda entry: entry['id'] not in seen_ids, feedparser.parse( "https://www.archlinux.org/feeds/news/").entries)))) # if no unread news, return if not news_to_show: return # show unseen news aurman_status("There are {} unseen news on archlinux.org".format( Colors.BOLD(Colors.LIGHT_MAGENTA(len(news_to_show))))) for entry in news_to_show: aurman_note("{} [{}]".format( Colors.BOLD(Colors.LIGHT_MAGENTA(entry['title'])), entry['published']), new_line=True) print(re.sub('<[^<]+?>', '', entry['summary'])) if ask_user("Have you read the news?", False, True): with open(seen_ids_file, 'a') as seenidsfile: seenidsfile.write( '\n'.join([entry['id'] for entry in news_to_show]) + '\n') else: logging.error( "User did not read the unseen news, but wanted to install packages on the system" ) raise InvalidInput( "User did not read the unseen news, but wanted to install packages on the system" )
def read_config() -> 'configparser.ConfigParser': """ Reads the aurman config and returns it :return: The aurman config """ # config dir config_dir = os.path.join( os.environ.get("XDG_CONFIG_HOME", os.path.expanduser(os.path.join("~", ".config"))), "aurman") config_file = os.path.join(config_dir, "aurman_config") # create config dir if it does not exist if not os.path.exists(config_dir): if run("install -dm700 '{}'".format(config_dir), shell=True, stdout=DEVNULL, stderr=DEVNULL).returncode != 0: logging.error("Creating config dir of aurman failed") raise InvalidInput("Creating config dir of aurman failed") # create empty config if config does not exist if not os.path.isfile(config_file): open(config_file, 'a').close() config = configparser.ConfigParser(allow_no_value=True) # make the config case sensitive config.optionxform = str # read the config config.read(config_file) AurmanConfig.aurman_config = config return config
def append_operation(operation: str): if args_to_return.operation is not None: logging.error("Tried to define more than one operation") raise InvalidInput("Tried to define more than one operation") args_to_return.operation = pacman_operations[operation] return "targets", 2
def read_config() -> 'configparser.ConfigParser': """ Reads the aurman config and returns it :return: The aurman config """ # config dir config_dir = os.path.join( os.environ.get("XDG_CONFIG_HOME", os.path.expanduser(os.path.join("~", ".config"))), "aurman") config_file = os.path.join(config_dir, "aurman_config") # create config dir if it does not exist if not os.path.exists(config_dir): try: os.makedirs(config_dir, mode=0o700, exist_ok=True) except OSError: logging.error( "Creating config dir of aurman {} failed".format(config_dir)) raise InvalidInput( "Creating config dir of aurman {} failed".format(config_dir)) # create empty config if config does not exist if not os.path.isfile(config_file): open(config_file, 'a').close() config = configparser.ConfigParser(allow_no_value=True) # make the config case sensitive config.optionxform = str # read the config config.read(config_file) AurmanConfig.aurman_config = config return config
def split_query_helper(max_length: int, base_length_of_query: int, length_per_append: int, to_append: Sequence[str]) -> \ List[List[str]]: """ Helper for splitting long queries. :param max_length: The max length of a query :param base_length_of_query: The base length of the query, e.g. length of expac -S '...' :param length_per_append: A constant which is being added to the query length for every append of a parameter e.g. 1 for a space :param to_append: A sequence containing the parameters as str :return: A list of lists where the inner lists contain the parameters for a single query len(return_value) yields the number of queries which have to be done all in all """ current_query_length = base_length_of_query current_list = [] return_list = [current_list] for append in to_append: append_length = len(append.encode("utf8")) if current_query_length + append_length + length_per_append <= max_length: current_list.append(append) current_query_length += append_length + length_per_append else: current_list = [append] return_list.append(current_list) current_query_length = base_length_of_query + append_length + length_per_append if current_query_length > max_length: logging.error("Query too long because of '{}'".format(append)) raise InvalidInput("Query too long because of '{}'".format(append)) return return_list
def makepkg(options_as_list: List[str], fetch_output: bool, dir_to_execute: str) -> List[str]: """ makepkg wrapper. see: https://www.archlinux.org/pacman/makepkg.8.html provide the makepkg options as string via "options_as_string". e.g. "--printsrcinfo" :param options_as_list: the makepkg options as string :param fetch_output: True if you want to receive the output of makepkg, False otherwise :param dir_to_execute: provide the directory in which the makepkg command should be executed :return: empty list in case of "fetch_output"=False, otherwise the lines of the makepkg output as list. one line of output is one item in the list. """ makepkg_query = ["makepkg"] + options_as_list if fetch_output: makepkg_return = run(makepkg_query, stdout=PIPE, universal_newlines=True, cwd=dir_to_execute) else: makepkg_return = run(makepkg_query, cwd=dir_to_execute) if makepkg_return.returncode != 0: logging.error("makepkg query {} failed in directory {}".format( makepkg_query, dir_to_execute)) raise InvalidInput("makepkg query {} failed in directory {}".format( makepkg_query, dir_to_execute)) if fetch_output: return makepkg_return.stdout.strip().splitlines() return []
def expac(option: str, formatting: Sequence[str], targets: Sequence[str]) -> List[str]: """ expac wrapper. see: https://github.com/falconindy/expac provide "option", "formatting" and "targets" as in this example: option as string: "-S" formatting as sequence containing strings: ("n", "v") targets as sequence containing strings: ("package1", "package2") :param option: option as in https://github.com/falconindy/expac :param formatting: formatting as in https://github.com/falconindy/expac :param targets: sequence containing strings as targets as in https://github.com/falconindy/expac :return: list containing the lines of the expac output. one line of output is one item in the list. the formatters are joined by '?!', so ('n', 'v') becomes %n?!%v in the output """ max_query_length = 131071 expac_base_query = "expac {} '{}'".format(option, "?!".join(["%{}".format(formatter) for formatter in formatting])) base_query_length = len(expac_base_query.encode("utf8")) queries_parameters = split_query_helper(max_query_length, base_query_length, 1, targets) return_list = [] for query_parameters in queries_parameters: query = "{}{}".format(expac_base_query, ''.join([" {}".format(parameter) for parameter in query_parameters])) expac_return = run(query, shell=True, stdout=PIPE, stderr=DEVNULL, universal_newlines=True) if expac_return.returncode != 0: logging.error("expac query {} failed".format(query)) raise InvalidInput("expac query {} failed".format(query)) return_list.extend(expac_return.stdout.strip().splitlines()) return return_list
def pacman_conf(option_as_string: str) -> List[str]: """ returns all values for a given option as received by executing pacman-conf e.g. calling with "HoldPkg" returns all "hold packages" declared in the pacman.conf :param option_as_string: the option to receive the values for :return: the values for the given option as strings in a list """ pacman_conf_return = run("pacman-conf", shell=True, stdout=PIPE, stderr=DEVNULL, universal_newlines=True) if pacman_conf_return.returncode != 0: logging.error("pacman-conf not available") raise InvalidInput("pacman-conf not available") to_return: List[str] = [] for line in pacman_conf_return.stdout.strip().splitlines(): to_split_at: str = "{} =".format(option_as_string) if line.startswith(to_split_at): to_return.append(line.split(to_split_at)[1].strip()) return to_return
def pacman(options_as_string: str, fetch_output: bool, dir_to_execute: str = None, sudo: bool = True, use_ask: bool = False) -> List[str]: """ pacman wrapper. see: https://www.archlinux.org/pacman/pacman.8.html provide the pacman options as string via "options_as_string". e.g. "-Syu package1 package2" :param options_as_string: the pacman options as string :param fetch_output: True if you want to receive the output of pacman, False otherwise :param dir_to_execute: if you want to execute the pacman command in a specific directory, provide the directory :param sudo: True if you want to execute pacman with sudo, False otherwise :param use_ask: Use --ask=4 when calling pacman, see: https://git.archlinux.org/pacman.git/commit/?id=90e3e026d1236ad89c142b427d7eeb842bbb7ff4 :return: empty list in case of "fetch_output"=False, otherwise the lines of the pacman output as list. one line of output is one item in the list. """ if use_ask: options_as_string += " --ask=4" if sudo: pacman_query = "sudo pacman {}".format(options_as_string) else: pacman_query = "pacman {}".format(options_as_string) if fetch_output: if dir_to_execute is not None: pacman_return = run(pacman_query, shell=True, stdout=PIPE, stderr=DEVNULL, universal_newlines=True, cwd=dir_to_execute) else: pacman_return = run(pacman_query, shell=True, stdout=PIPE, stderr=DEVNULL, universal_newlines=True) else: if dir_to_execute is not None: pacman_return = run(pacman_query, shell=True, cwd=dir_to_execute) else: pacman_return = run(pacman_query, shell=True) if pacman_return.returncode != 0: logging.error("pacman query {} failed".format(pacman_query)) raise InvalidInput("pacman query {} failed".format(pacman_query)) if fetch_output: return pacman_return.stdout.strip().splitlines() return []
def packages_from_other_sources() -> Tuple[Set[str], Dict[str, str]]: """ Returns the packages which should be installed from sources where they normally would not be installed from. :return: A tuple containing two items: First item: Set containing the names of packages to install from the aur Second item: Dict containing names from known repo packages as keys and the repo to install those packages from as values """ config = AurmanConfig.aurman_config if config is None: aurman_error("aurman config not loaded") raise InvalidInput("aurman config not loaded") aur_set = set() if 'aur_packages' in config: for aur_package_name in config['aur_packages']: aur_set.add(aur_package_name) repo_dict = {} if 'repo_packages' in config: for repo_package_name in config['repo_packages']: if config['repo_packages'][repo_package_name] is None: continue repo_dict[repo_package_name] = str( config['repo_packages'][repo_package_name]) for name in aur_set: if name in repo_dict: aurman_error("Package {} listed for aur and repo.".format( Colors.BOLD(Colors.LIGHT_MAGENTA(name)))) raise InvalidInput("Package {} listed for aur and repo.".format( Colors.BOLD(Colors.LIGHT_MAGENTA(name)))) return aur_set, repo_dict
def acquire_sudo(): """ sudo loop since we want sudo forever """ def sudo_loop(): while True: if run(["sudo", "--non-interactive", "-v"]).returncode != 0: logging.error("acquire sudo failed") time.sleep(SudoLoop.timeout) if run(["sudo", "-v"]).returncode != 0: logging.error("acquire sudo failed") raise InvalidInput("acquire sudo failed") t = threading.Thread(target=sudo_loop) t.daemon = True t.start()
def acquire_sudo(): """ sudo loop since we want sudo forever """ def sudo_loop(): while True: if run("sudo -v", shell=True, stdout=DEVNULL).returncode != 0: logging.error("acquire sudo failed") time.sleep(120) if run("sudo -v", shell=True).returncode != 0: logging.error("acquire sudo failed") raise InvalidInput("acquire sudo failed") t = threading.Thread(target=sudo_loop) t.daemon = True t.start()
def get_aur_info(package_names: Sequence[str], search: bool = False, by_name: bool = False) -> List[Dict]: """ Fetches AUR infos for package_names via AurJson. https://wiki.archlinux.org/index.php/AurJson :param package_names: The names of the packages in a sequence :param search: True if one wants to search instead of getting info :param by_name: If one wants to search by name only :return: A list containing the "results" values of the RPC answer. """ max_query_length = 8000 if not search: query_url = aur_domain + "/rpc/?v=5&type=info" query_prefix = "&arg[]=" else: query_url = aur_domain + "/rpc/?v=5&type=search" if by_name: query_url += "&by=name" query_prefix = "&arg=" query_url_length = len(query_url.encode("utf8")) query_prefix_length = len(query_prefix.encode("utf8")) # quote_plus needed for packages like libc++ package_names = [quote_plus(package_name) for package_name in package_names] queries_parameters = split_query_helper(max_query_length, query_url_length, query_prefix_length, package_names) results_list = [] for query_parameters in queries_parameters: try: results_list.extend(json.loads(requests.get("{}{}".format(query_url, ''.join( ["{}{}".format(query_prefix, parameter) for parameter in query_parameters])), timeout=5).text)[ 'results']) except requests.exceptions.RequestException: logging.error("Connection problem while requesting AUR info for {}".format(package_names), exc_info=True) raise ConnectionProblem("Connection problem while requesting AUR info for {}".format(package_names)) except json.JSONDecodeError: logging.error("Decoding problem while requesting AUR info for {}".format(package_names), exc_info=True) raise InvalidInput("Decoding problem while requesting AUR info for {}".format(package_names)) return results_list
def expac(option: str, formatting: Sequence[str], targets: Sequence[str]) -> List[str]: """ expac wrapper. see: https://github.com/falconindy/expac provide "option", "formatting" and "targets" as in this example: option as string: "-S" formatting as sequence containing strings: ("n", "v") targets as sequence containing strings: ("package1", "package2") :param option: option as in https://github.com/falconindy/expac :param formatting: formatting as in https://github.com/falconindy/expac :param targets: sequence containing strings as targets as in https://github.com/falconindy/expac :return: list containing the lines of the expac output. one line of output is one item in the list. the formatters are joined by '?!', so ('n', 'v') becomes %n?!%v in the output """ cmd = [ "expac", option, "?!".join(["%{}".format(formatter) for formatter in formatting]) ] if targets: cmd += ["-"] expac_return = run(cmd, input='\n'.join(targets), stdout=PIPE, stderr=DEVNULL, universal_newlines=True) if expac_return.returncode != 0: query_stringified = ' '.join(quote(i) for i in cmd[1:]) logging.error("expac query {} for targets {} failed".format( query_stringified, targets)) raise InvalidInput("expac query {} for targets {} failed".format( query_stringified, targets)) return expac_return.stdout.strip().splitlines()
def search_and_print(names: Sequence[str], installed_system, pacman_params: str, repo: bool, aur: bool): """ Searches for something and prints the results :param names: The things to search for :param installed_system: A system containing the installed packages :param pacman_params: parameters for pacman as string :param repo: search only in repo :param aur: search only in aur """ if not names: return if not aur: # escape for pacman to_escape = list("()+?|{}") for char in to_escape: pacman_params = pacman_params.replace(char, "\{}".format(char)) run("pacman {}".format(pacman_params), shell=True) if not repo: # see: https://docs.python.org/3/howto/regex.html regex_chars = list("^.+*?$[](){}\|") regex_patterns = [ regex.compile(name, regex.IGNORECASE) for name in names ] names_beginnings_without_regex = [] for name in names: index_start = -1 index_end = len(name) for i, char in enumerate(name): if char not in regex_chars and index_start == -1: index_start = i elif char in regex_chars and index_start != -1: # must be at least two consecutive non regex chars if i - index_start < 2: index_start = -1 continue index_end = i break if index_start == -1 or index_end - index_start < 2: aurman_error( "Your query {} contains not enough non regex chars!". format(Colors.BOLD(Colors.LIGHT_MAGENTA(name)))) raise InvalidInput( "Your query {} contains not enough non regex chars!". format(Colors.BOLD(Colors.LIGHT_MAGENTA(name)))) names_beginnings_without_regex.append(name[index_start:index_end]) found_names = set( ret_dict['Name'] for ret_dict in get_aur_info( [names_beginnings_without_regex[0]], True) if regex_patterns[0].findall(ret_dict['Name']) or isinstance(ret_dict['Description'], str) and regex_patterns[0].findall(ret_dict['Description'])) for i in range(1, len(names)): found_names &= set( ret_dict['Name'] for ret_dict in get_aur_info( [names_beginnings_without_regex[i]], True) if regex_patterns[i].findall(ret_dict['Name']) or isinstance(ret_dict['Description'], str) and regex_patterns[i].findall(ret_dict['Description'])) search_return = get_aur_info(found_names) for ret_dict in sorted(search_return, key=lambda x: float(x['Popularity']), reverse=True): repo_with_slash = Colors.BOLD(Colors.LIGHT_MAGENTA("aur/")) name = Colors.BOLD(ret_dict['Name']) if ret_dict['OutOfDate'] is None: version = Colors.BOLD(Colors.GREEN(ret_dict['Version'])) else: version = Colors.BOLD(Colors.RED(ret_dict['Version'])) first_line = "{}{} {} ({}, {})".format(repo_with_slash, name, version, ret_dict['NumVotes'], ret_dict['Popularity']) if ret_dict['Name'] in installed_system.all_packages_dict: if version_comparison( ret_dict['Version'], "=", installed_system.all_packages_dict[ ret_dict['Name']].version): first_line += " {}".format( Colors.BOLD(Colors.CYAN("[installed]"))) else: first_line += " {}".format( Colors.BOLD( Colors.CYAN("[installed: {}]".format( installed_system.all_packages_dict[ ret_dict['Name']].version)))) print(first_line) print(" {}".format(ret_dict['Description']))
def parse_pacman_args(args: Sequence[str]) -> 'PacmanArgs': """ Own parsing. Fills a concrete instance of PacmanArgs :param args: the args to parse :return: the instance of PacmanArgs containing the parsed parameters """ def append_operation(operation: str): if args_to_return.operation is not None: logging.error("Tried to define more than one operation") raise InvalidInput("Tried to define more than one operation") args_to_return.operation = pacman_operations[operation] return "targets", 2 def append_bool(param: str): new_current_field = pacman_options[param][0] # dirty hack for things like -yy or -cc if hasattr(args_to_return, new_current_field): setattr(args_to_return, new_current_field, ['something']) else: setattr(args_to_return, new_current_field, True) return new_current_field, 0 args_to_return = PacmanArgs() current_field = "targets" number_of_valid_arguments = 2 only_targets = False multiple_append_allowed = False for arg in args: arg_length = len(arg) if only_targets: dashes = 0 elif arg == '--': only_targets = True current_field = "targets" number_of_valid_arguments = 2 continue elif arg.startswith("-"): if arg.startswith("--"): dashes = 2 else: dashes = 1 else: dashes = 0 arg = arg.replace("-", "", dashes) if dashes == 2: if arg_length < 4: logging.error("{} is too short".format(arg)) raise InvalidInput("{} is too short".format(arg)) if arg in pacman_options and pacman_options[arg][4]: multiple_append_allowed = True elif dashes == 1: if arg_length < 2: logging.error("{} is too short".format(arg)) raise InvalidInput("{} is too short".format(arg)) if arg_length > 2: for i in range(0, len(arg) - 1): curr_char = arg[i] if curr_char in pacman_operations: current_field, number_of_valid_arguments = append_operation(curr_char) elif curr_char not in pacman_options or pacman_options[curr_char][1] != 0: args_to_return.invalid_args.append(curr_char) else: current_field, number_of_valid_arguments = append_bool(curr_char) arg = arg[len(arg) - 1] if arg in pacman_options and pacman_options[arg][4]: multiple_append_allowed = True else: if isinstance(getattr(args_to_return, current_field), bool) or \ (not multiple_append_allowed and number_of_valid_arguments < 2 and len(getattr(args_to_return, current_field)) + 1 > number_of_valid_arguments): current_field = "targets" number_of_valid_arguments = 2 getattr(args_to_return, current_field).append(arg) multiple_append_allowed = False if dashes > 0: if arg not in pacman_options or not pacman_options[arg][4]: multiple_append_allowed = False if arg in pacman_operations: current_field, number_of_valid_arguments = append_operation(arg) elif arg not in pacman_options: args_to_return.invalid_args.append(arg) elif pacman_options[arg][1] == 0: current_field, number_of_valid_arguments = append_bool(arg) else: current_field = pacman_options[arg][0] number_of_valid_arguments = pacman_options[arg][1] if not hasattr(args_to_return, current_field): setattr(args_to_return, current_field, []) if args_to_return.operation is None: logging.error("No operation defined") raise InvalidInput("No operation defined") checked_fields = set() for pacman_option in pacman_options.values(): current_field = pacman_option[0] if current_field in checked_fields: continue number_of_valid_arguments = pacman_option[1] if not hasattr(args_to_return, current_field): if number_of_valid_arguments == 0: setattr(args_to_return, current_field, False) else: setattr(args_to_return, current_field, []) else: if number_of_valid_arguments > 0 and not getattr(args_to_return, current_field): logging.error("Parameters for {} are needed".format(current_field)) raise InvalidInput("Parameters for {} are needed".format(current_field)) checked_fields.add(current_field) return args_to_return