#!/usr/bin/python3 """Main installer file. Shamelessly copied from https://github.com/dmerejkowsky/dotfiles. """ import argparse import subprocess from typing import List, Optional from urllib.request import urlretrieve import cli_ui as ui # type: ignore from path import Path # type: ignore import ruamel.yaml class Installer: """Regroups all the installation methods listed in the yaml conf file.""" def __init__(self, force: bool = False, update: bool = False): self.conf = ruamel.yaml.safe_load( # type: ignore Path("configs.yml").text() ) self.base_dir = Path.getcwd() self.home = Path("~").expanduser() self.force = force self.update = update def pretty_path(self, path: Path) -> str: """Put the ~/ back in the path.""" relpath: str = path.relpath(self.home) return "~/" + relpath def do_clone(self, url: str, dest: str, branch: str = "master") -> None: """Do the git clone.""" p_dest = Path(dest).expanduser() pretty_dest = self.pretty_path(p_dest) p_dest.parent.makedirs_p() if p_dest.exists(): if self.force: ui.info_2("Removing", pretty_dest) p_dest.rmtree() elif self.update: ui.info_2("Updating", pretty_dest) self.do_run("cd {} && git pull".format(p_dest)) return else: ui.info_2("Skipping", pretty_dest) return ui.info_2("Cloning", url, "->", pretty_dest) git_command = "git clone {} {} --branch {}".format(url, p_dest, branch) subprocess.run(git_command, check=True, shell=True) def do_copy(self, src: str, dest: str) -> None: """Copy two files.""" p_dest = Path(dest).expanduser() pretty_dest = self.pretty_path(p_dest) p_dest.parent.makedirs_p() if p_dest.exists() and not self.force: ui.info_2("Skipping", pretty_dest) return p_src: Path = self.base_dir / "dotfiles" / src ui.info_2("Copy", p_src, "->", self.pretty_path(p_src)) p_src.copy(p_dest) def do_download( self, *, url: str, dest: str, executable: bool = False ) -> None: """Retrieve a file from a url.""" p_dest = Path(dest).expanduser() pretty_dest = self.pretty_path(p_dest) p_dest.parent.makedirs_p() if p_dest.exists() and not self.force: ui.info_2("Skipping", pretty_dest) else: ui.info_2("Fetching", url, "->", pretty_dest) urlretrieve(url, p_dest) if executable: p_dest.chmod(0o755) def do_run(self, command: str, condition: str = 'true') -> None: """Run a command.""" if not evaluate_condition(condition): return ui.info_2("Running", "`{}`".format(command)) subprocess.run(command, check=True, shell=True) self.base_dir.chdir() def do_symlink(self, src: str, dest: str) -> None: """Make a symlink to a file.""" self._do_symlink(src, dest, is_dir=False) def do_symlink_dir(self, src: str, dest: str) -> None: """Make a symlink to a dir.""" self._do_symlink(src, dest, is_dir=True) def _do_symlink(self, src: str, dest: str, *, is_dir: bool) -> None: p_src = Path(src).expanduser() pretty_src = self.pretty_path(p_src) p_dest = Path(dest).expanduser() pretty_dest = self.pretty_path(p_dest) if is_dir: p_dest.parent.parent.makedirs_p() else: p_dest.parent.makedirs_p() if p_dest.exists() and not self.force: ui.info_2("Skipping", pretty_dest) return if p_dest.islink(): p_dest.remove() ui.info_2("Symlink", pretty_dest, "->", pretty_src) if p_src.startswith("/"): src_full = p_src else: src_full = self.base_dir / "dotfiles" / p_src src_full.symlink(p_dest) def do_run_update(self, command: str) -> None: """Run a command only if self.update is True.""" if self.update: self.do_run(command) else: ui.info_2("Skipping", "`{}`".format(command)) def do_run_no_update(self, command: str) -> None: """Run a command only if self.update is False.""" if not self.update: self.do_run(command) else: ui.info_2("Skipping", "`{}`".format(command)) def do_append(self, dest: str, content: str) -> None: """Append to a file.""" self._do_write(dest, content, append=True) def do_write(self, dest: str, content: str) -> None: """Write into a file.""" self._do_write(dest, content, append=False) def _do_write(self, dest: str, content: str, *, append: bool) -> None: p_dest = Path(dest).expanduser() pretty_dest = self.pretty_path(p_dest) if p_dest.exists() and not self.force: ui.info_2("Skipping", pretty_dest) return ui.info_2("Creating", pretty_dest) p_dest.parent.makedirs_p() content = content.format(base_dir=self.base_dir, home=self.home) if not content.endswith("\n"): content += "\n" p_dest.write_text(content, append=append) def install(self, programs: Optional[List[str]] = None) -> None: """Install the programs provided. If no program is provided, use the conf file to find the programs. """ if not programs: programs = sorted(self.conf.keys()) for program in programs: self.install_program(program) def install_program(self, program: str) -> None: """Install one program (called by self.install()). Call indivdual methods from the conf file. """ ui.info(ui.green, program) ui.info("-" * len(program)) todo = self.conf[program] for action in todo: name = list(action.keys())[0] params = action[name] func = getattr(self, "do_{}".format(name)) if isinstance(params, dict): func(**params) else: func(*params) ui.info() def evaluate_condition(command: str) -> bool: """Run a bash command. On success return True, else return False.""" return subprocess.run(command, check=False, shell=True).returncode == 0 def main() -> None: """Parse args and instantiate the Installer.""" parser = argparse.ArgumentParser() parser.add_argument( "programs", nargs="*", ) parser.add_argument( "--force", action="store_true", help="Overwrite existing files", ) parser.add_argument( "--update", action="store_true", help="Update git repos", ) args = parser.parse_args() programs = args.programs force = args.force update = args.update installer = Installer(force=force, update=update) installer.install(programs=programs) if __name__ == "__main__": main()