def run(): """Run sanity tests on package files""" message.info("Checking for package errors...") groups = Group.load_all() errors_found = 0 for group in groups: files = group.files packages = group.packages for package in packages: errors_found += package.evaluate() for _file in package.files: errors_found += _file.evaluate() for _file in files: errors_found += _file.evaluate() if errors_found > 0: message.alert( f"{errors_found} errors found. It is highly recommended to fix them before proceeding." ) if not message.question("Proceed without fixing?", "boolean"): sys.exit(1) else: message.info("No errors found") message.break_line()
def link(self): """Link this file from source to destination""" if self.path_source is not None: full_source_path = os.path.join( os.path.expandvars(self.path_source), self.name) full_destination_path = os.path.join( os.path.expandvars(self.path_destination), self.name) try: if self.sudo: spawn.process( f'ln -sfv "{full_source_path}" "{full_destination_path}"', sudo=True, ) else: os.symlink(full_source_path, full_destination_path) except FileExistsError: message.error( "Can't symlink, file already exists at destination. Attempting fix." ) os.remove(full_destination_path) message.info(f"Removed: '{full_destination_path}'") os.symlink(full_source_path, full_destination_path) finally: message.info( f"Symlink created: '{full_source_path}' <--> '{full_destination_path}'" ) else: message.error( f"'{self.name}' has no source from which to create a link from." )
def process(process_string, silent=False, capture=False, sudo=False): """Run a process. If silent = False, displays process output to the screen, otherwise only this program's output is shown. If capture = True, return the process' stdout instead of a CompletedProcess instance. If sudo = True, run the process as super user (may require user intervention for collecting password). On error, stops execution for 30 seconds for evaluation, alternatively, crashes the program. """ if sudo: process_string = f"{config.admin_command} {process_string}" [process_name, *args] = process_string.split() # Remove special {SPACE} character, which denotes a space that doesn't mean a new argument for index, arg in enumerate(args): if "{SPACE}" in arg: args[index] = arg.replace("{SPACE}", " ") try: output = run( [process_name, *args], check=True, text=True, capture_output=(capture or silent), ) except CalledProcessError as error: message.error( f"'{process_name}' failed with code {error.returncode} :: {error.output}" ) message.alert(f"ARGS: {args}") if config.fail_fast: message.alert( f"Halting execution because of fail_fast = {config.fail_fast}") sys.exit(1) else: message.info("Stopping execution temporarily for your evaluation.") for i in range(3, 0, -1): message.info(f"Program will continue in {i * 10} seconds...") sleep(config.seconds_to_wait_on_fail) output = run(["echo"], check=True, text=True) if capture: return output.stdout return output
def show_comments(self): """Show useful comments to the user, such as how to configure a program further or what to do next. """ for index, comment in enumerate(self.comments): if index == 0: message.heading(f"[{self.name}]") message.info(comment) if len(self.comments) > 0: log.write(f"FILE ({self.name})\n" + "\n".join(self.comments))
def copy(self): """Copy this file from source to destination""" if self.path_source is not None: full_source_path = os.path.join( os.path.expandvars(self.path_source), self.name) if self.sudo: spawn.process( f'cp -v -- "{full_source_path}" "{self.path_destination}"', sudo=True, ) else: message.info( f"Copied: '{full_source_path}' --> '{self.path_destination}'" ) shutil.copy(full_source_path, self.path_destination) else: message.error( f"'{self.name}' has no source from which to copy from.")
def touch(self): """Create this file at its destination. Write text to it.""" full_destination_path = os.path.join( os.path.expandvars(self.path_destination), self.name) try: with open(full_destination_path, "w", encoding="utf-8") as _file: _file.write(self.text) message.info( f"Created file: '{self.name}' at '{self.path_destination}'") except OSError: message.error( f"There was a problem creating the file '{self.name}' at '{self.path_destination}'" ) if config.fail_fast: sys.exit(1) message.info("Stopping execution temporarily for your evaluation.") for i in range(3, 0, -1): message.info(f"Program will continue in {i * 10} seconds...") sleep(config.seconds_to_wait_on_fail)
def main(): """Start here.""" if config.dev_mode: print("DEV MODE: ON") test.run() while True: message.heading("Welcome! Pick your poison:") choice = message.choose( [ "Full install", "Select group", "Create links", "Create new package group", "Populate main.json with all group .json files", ], allow_exit=True, ) if choice == "Full install": log.write("--LOG START--", refresh=True) groups = Group.load_all() for index, group in enumerate(groups, start=1): if index == 1: group.install(sync=True) else: group.install() log.write("--LOG END--") elif choice == "Select group": groups = Group.load_all() group_choice = message.choose([group.name for group in groups], allow_exit=True) if group_choice != "EXIT": Group.load(group_choice).install() else: os.system("clear") elif choice == "Create links": groups = Group.load_all() group_choice = message.choose([group.name for group in groups], allow_exit=True) if group_choice != "EXIT": Group.load(group_choice).link_files() else: os.system("clear") elif choice == "Create new package group": Group.interactive_insert().save() message.info( "Don't forget to add this group to the 'main.json' file. This decides the order in which it is installed." ) elif choice == "Populate main.json with all group .json files": group_names = [ group.replace(".json", "") for group in os.listdir(config.group_files_path) if group.endswith(".json") and group != "initial.json" ] group_names.insert(0, "initial") with open( os.path.join(config.program_path, "data", "main.json"), "w", encoding="utf-8", ) as _file: json.dump(group_names, fp=_file, indent=4) os.system("clear") message.info("Populated main.json with all groups found.") else: message.normal("See ya!") break
def interactive_insert(group_name=None, package_name=None): """Create a new file object from the command line""" file_name = "" file_destination = "" file_source = None file_create_link = False file_sudo = False file_comments = [] def ask_file_source(): return message.question("What is the full source file path?") def ask_sudo(): return message.question("Is sudo needed for this operation?", "boolean") while True: file_source = None file_create_link = False file_sudo = False message.heading( "Creating a new file. (${vars} is supported, '~' is not)") if group_name is not None: message.info(f"Current group: {group_name}") if package_name is not None: message.info(f"Current package: {package_name}") file_destination = message.question( "Where will this file be (created/linked/copied) to? (no basename)" ) if message.question("Will this file be linked to [destination]?", "boolean"): file_create_link = True file_source = ask_file_source() file_sudo = ask_sudo() elif message.question("Will this file be copied to [destination]?", "boolean"): file_source = ask_file_source() file_sudo = ask_sudo() if file_source is not None: [_, file_name] = os.path.split(os.path.expandvars(file_source)) else: file_name = message.question("What will be the file's name?") if message.question("Will the file have comments to aid the user?", "boolean"): while True: comment = message.question("New comment:") file_comments.append(comment) if not message.question("Add another comment?", "boolean"): break new_file = File( file_name, file_destination, os.path.split(file_source)[0] if file_source is not None else None, "", file_create_link, file_sudo, file_comments, ) new_file.evaluate() message.info(f"""File info: [Name]: '{new_file.name}' [Destination]: '{new_file.path_destination}' [Source]: '{new_file.path_source}' [Link?]: '{'Yes' if new_file.create_link else 'No'}' [Need superuser?]: '{'Yes' if new_file.sudo else 'No'}' [Comments]: {new_file.comments}""") if message.question("Confirm?", "boolean"): break return new_file
def interactive_insert(group_name=None): """Create a new package object from the command line""" package_display_name = "" package_name = "" package_installer = config.default_installer package_files = [] package_auto_start = [] package_comments = [] package_post_install_commands = [] while True: message.heading("Creating a new package.") if group_name is not None: message.info(f"Current group: {group_name}") package_display_name = message.question( "What is the display name of the package?") package_name = message.question( "What is the exact name of the package? (Will be used for installing, needs to be exact)" ) package_installer = message.choose( ["default", "pacman", "aur", "pip"]) if package_installer == "default": package_installer = config.default_installer if message.question("Will this package be autostarted?", "boolean"): while True: command = message.question( "New command for autostart (will appear on a newline):" ) package_auto_start.append(command) if not message.question("Add another autostart command?", "boolean"): break if message.question( "Will this package have post install commands?", "boolean"): while True: command = message.question( "New command for post install (${vars} supported):") package_post_install_commands.append(command) if not message.question("Add another command?", "boolean"): break if message.question("Will this package have files associated?", "boolean"): while True: _file = File.interactive_insert(group_name, package_name).to_dict() package_files.append(_file) if not message.question("Add another file?", "boolean"): break if message.question( "Will the package have comments to aid the user?", "boolean"): while True: comment = message.question("New comment:") package_comments.append(comment) if not message.question("Add another comment?", "boolean"): break package = Package( package_display_name, package_name, package_installer, package_files, package_auto_start, package_comments, package_post_install_commands, ) package.evaluate() message.info(f"""Package info: [Display Name]: '{package.display_name}' [Install Name]: '{package.name}' [Installer]: '{package.installer}' [Autostart]: '{package.auto_start}' [Post Install Commands]: '{package.post_install_commands}' [Files]: '{[_file.name for _file in package.files]}' [Comments]: {package.comments}""") if message.question("Confirm?", "boolean"): break return package