diff --git a/install.py b/install.py new file mode 100644 index 0000000..09e7712 --- /dev/null +++ b/install.py @@ -0,0 +1,195 @@ +"""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 yml conf file.""" + + def __init__(self, force: 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 + + 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(dest) + p_dest.parent.makedirs_p() + + if p_dest.exists(): + if self.force: + p_dest.rmtree() + else: + ui.info_2("Skipping", pretty_dest) + return + + ui.info_2("Cloning", url, "->", pretty_dest) + git_command = ["git", "clone", url, p_dest, "--branch", branch] + subprocess.check_call(git_command) + + def do_copy(self, src: str, dest: str) -> None: + """Copy two files.""" + + p_dest = Path(dest).expanduser() + pretty_dest = self.pretty_path(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(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, args: List[str]) -> None: + """Run a command.""" + + ui.info_2("Running", "`{}`".format(" ".join(args))) + command = [x.format(home=self.home) for x in args] + subprocess.check_call(command) + + 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_dest = Path(dest).expanduser() + pretty_dest = self.pretty_path(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(): + # TODO: check fucntion for bash -ef equivalent + p_dest.remove() + + ui.info_2("Symlink", pretty_dest, "->", src) + src_full: Path = self.base_dir / "dotfiles" / src + src_full.symlink(p_dest) + + def do_write(self, dest: str, content: str) -> None: + """Write into a file.""" + + p_dest = Path(dest).expanduser() + pretty_dest = self.pretty_path(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) + + 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 main() -> None: + """Parse args and instantiate the Installer.""" + + parser = argparse.ArgumentParser() + parser.add_argument( + "programs", + nargs="*" + ) + parser.add_argument( + "--force", + action="strore_true", + help="Overwrite existing files" + ) + args = parser.parse_args() + + force = args.force + programs = args.programs + + installer = Installer(force=force) + installer.install(programs=programs) + + +if __name__ == "__main__": + main()