""" This script will install blogger-cli and its dependencies in isolation from the rest of the system. It does, in order: - Downloads the latest stable version of blogger-cli. - Downloads all its dependencies in the blogger-cli/venv directory. - Copies it and all extra files in $BLOGGER_CLI_HOME. - Updates the PATH in a system-specific way. There will be a `blogger` script that will be installed in $BLOGGER_CLI_HOME/bin """ import argparse import os import platform import shutil import stat import subprocess import sys from contextlib import closing from io import UnsupportedOperation try: from urllib.request import Request from urllib.request import urlopen except ImportError: from urllib2 import Request from urllib2 import urlopen try: input = raw_input except NameError: pass try: try: import winreg except ImportError: import _winreg as winreg except ImportError: winreg = None WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt") FOREGROUND_COLORS = { "black": 30, "red": 31, "green": 32, "yellow": 33, "blue": 34, "magenta": 35, "cyan": 36, "white": 37, } BACKGROUND_COLORS = { "black": 40, "red": 41, "green": 42, "yellow": 43, "blue": 44, "magenta": 45, "cyan": 46, "white": 47, } OPTIONS = {"bold": 1, "underscore": 4, "blink": 5, "reverse": 7, "conceal": 8} def style(fg, bg, options): codes = [] if fg: codes.append(FOREGROUND_COLORS[fg]) if bg: codes.append(BACKGROUND_COLORS[bg]) if options: if not isinstance(options, (list, tuple)): options = [options] for option in options: codes.append(OPTIONS[option]) return "\033[{}m".format(";".join(map(str, codes))) STYLES = { "info": style("green", None, None), "comment": style("yellow", None, None), "error": style("red", None, None), "warning": style("yellow", None, None), } def is_decorated(): if platform.system().lower() == "windows": return ( os.getenv("ANSICON") is not None or os.getenv("ConEmuANSI") == "ON" or os.getenv("Term") == "xterm" ) if not hasattr(sys.stdout, "fileno"): return False try: return os.isatty(sys.stdout.fileno()) except UnsupportedOperation: return False def is_interactive(): if not hasattr(sys.stdin, "fileno"): return False try: return os.isatty(sys.stdin.fileno()) except UnsupportedOperation: return False def colorize(style, text): if not is_decorated(): return text return "{}{}\033[0m".format(STYLES[style], text) def string_to_bool(value): value = value.lower() return value in {"true", "1", "y", "yes"} def expanduser(path): """ Expand ~ and ~user constructions. Includes a workaround for http://bugs.python.org/issue14768 """ expanded = os.path.expanduser(path) if path.startswith("~/") and expanded.startswith("//"): expanded = expanded[1:] return expanded HOME = expanduser("~") LINUX_HOME = os.path.join(HOME, "local", ".blogger_cli") WINDOWS_HOME = os.path.join(HOME, ".blogger_cli") BLOGGER_CLI_HOME = WINDOWS_HOME if WINDOWS else LINUX_HOME BLOGGER_CLI_BIN = os.path.join(BLOGGER_CLI_HOME, "bin") BLOGGER_CLI_ENV = os.path.join(BLOGGER_CLI_HOME, "env") BLOGGER_CLI_VENV = os.path.join(BLOGGER_CLI_HOME, "venv") BLOGGER_CLI_VENV_BACKUP = os.path.join(BLOGGER_CLI_HOME, "venv-backup") BIN = """#!{python_path} from blogger_cli.cli import cli if __name__ == "__main__": cli() """ BAT = '@echo off\r\n{python_path} "{blogger_cli_bin}" %*\r\n' PRE_MESSAGE = """# Welcome to {blogger-cli}! This will download and install the latest version of {blogger-cli}, a ipynb converter and blog manager. It will add the `blogger` command to {blogger-cli}'s bin directory, located at: {blogger_cli_home_bin} {platform_msg} You can uninstall at any time by executing this script with the --uninstall option, and these changes will be reverted. """ PRE_UNINSTALL_MESSAGE = """# We are sorry to see you go! This will uninstall {blogger-cli}. It will remove the `blogger` command from {blogger-cli}'s bin directory, located at: {blogger_cli_home_bin} This will also remove {blogger-cli} from your system's PATH. """ PRE_MESSAGE_UNIX = """This path will then be added to your `PATH` environment variable by modifying the profile file{plural} located at: {rcfiles}""" PRE_MESSAGE_WINDOWS = """This path will then be added to your `PATH` environment variable by modifying the `HKEY_CURRENT_USER/Environment/PATH` registry key.""" PRE_MESSAGE_NO_MODIFY_PATH = """This path needs to be in your `PATH` environment variable, but will not be added automatically.""" POST_MESSAGE_UNIX = """{blogger-cli} is installed now. Great! To get started you need {blogger-cli}'s bin directory ({blogger_cli_home_bin}) in your `PATH` environment variable. Next time you log in this will be done automatically. You have to run blogger-cli using the 'blogger' command! If 'blogger' command doesnot work place {linux_addition} in your bashrc/bash_profile To configure your current shell run `source {blogger_cli_home_env}` """ POST_MESSAGE_WINDOWS = """{blogger-cli} is installed now. Great! To get started you need blogger-cli's bin directory ({blogger_cli_home_bin}) in your `PATH` environment variable. Future applications will automatically have the correct environment, but you may need to restart your current shell. You have to run blogger-cli using the 'blogger' command! """ POST_MESSAGE_WINDOWS_NO_MODIFY_PATH = """{blogger-cli} is installed now. Great! To get started you need Blogger-cli's bin directory ({blogger_cli_home_bin}) in your `PATH` environment variable. This has not been done automatically. You have to run blogger-cli using the 'blogger' command! """ class Installer: CURRENT_PYTHON = sys.executable CURRENT_PYTHON_VERSION = sys.version_info[:2] def __init__(self, version=None, force=False, accept_all=False): self._version = version self._force = force self._modify_path = True self._accept_all = accept_all def run(self): self.display_pre_message() self.ensure_python_version() self.ensure_home() try: self.install() except subprocess.CalledProcessError as e: print(colorize("error", "An error has occured: {}".format(str(e)))) print(e.output.decode()) return e.returncode self.display_post_message(self._version) return 0 def uninstall(self): self.display_pre_uninstall_message() if not self.customize_uninstall(): return self.remove_home() self.remove_from_path() def ensure_python_version(self): major, minor = self.CURRENT_PYTHON_VERSION if major < 3 or minor < 5: print("SORRY BLOGGER CLI IS NOT SUPPORTED ONLY FOR 3.5 AND ABOVE!") sys.exit(1) def customize_uninstall(self): if not self._accept_all: print() uninstall = ( input("Are you sure you want to uninstall Blogger-cli? (y/[n]) ") or "n" ) if uninstall.lower() not in {"y", "yes"}: return False print("") return True def ensure_home(self): """ Ensures that $BLOGGER_CLI_HOME exists or create it. """ if not os.path.exists(BLOGGER_CLI_HOME): os.makedirs(BLOGGER_CLI_HOME, 0o755) def remove_home(self): """ Removes $BLOGGER_CLI_HOME. """ if not os.path.exists(BLOGGER_CLI_HOME): return shutil.rmtree(BLOGGER_CLI_HOME) def install(self): """ Installs Blogger-cli in $BLOGGER_CLI_HOME. """ version = self._version if self._version else "Latest" print("Installing version: " + colorize("info", version)) self.make_venv() self.make_bin() self.make_env() self.update_path() return 0 def make_venv(self): """ Packs everything into a single lib/ directory. """ if os.path.exists(BLOGGER_CLI_VENV_BACKUP): shutil.rmtree(BLOGGER_CLI_VENV_BACKUP) # Backup the current installation if os.path.exists(BLOGGER_CLI_VENV): shutil.copytree(BLOGGER_CLI_VENV, BLOGGER_CLI_VENV_BACKUP) if self._force: shutil.rmtree(BLOGGER_CLI_VENV) try: self._make_venv() except Exception: if not os.path.exists(BLOGGER_CLI_VENV_BACKUP): raise shutil.copytree(BLOGGER_CLI_VENV_BACKUP, BLOGGER_CLI_VENV) shutil.rmtree(BLOGGER_CLI_VENV_BACKUP) raise finally: if os.path.exists(BLOGGER_CLI_VENV_BACKUP): shutil.rmtree(BLOGGER_CLI_VENV_BACKUP) def _make_venv(self): global BIN, BAT if not os.path.exists(BLOGGER_CLI_VENV): import venv print("Making virtualenv in", BLOGGER_CLI_VENV) venv.create(BLOGGER_CLI_VENV, with_pip=True) windows_path = os.path.join(BLOGGER_CLI_VENV, "Scripts", "python") linux_path = os.path.join(BLOGGER_CLI_VENV, "bin", "python") new_python = windows_path if WINDOWS else linux_path new_pip = new_python + " -m pip" if self._version: install_cmd = new_pip + " install blogger-cli==" + self._version else: install_cmd = new_pip + " install -U blogger-cli" BIN = BIN.format(python_path=new_python) BAT = BAT.format(python_path=new_python, blogger_cli_bin="{blogger_cli_bin}") os.system(install_cmd) def make_bin(self): if not os.path.exists(BLOGGER_CLI_BIN): os.mkdir(BLOGGER_CLI_BIN, 0o755) if WINDOWS: with open(os.path.join(BLOGGER_CLI_BIN, "blogger.bat"), "w") as f: f.write( BAT.format( blogger_cli_bin=os.path.join( BLOGGER_CLI_BIN, "blogger" ).replace(os.environ["USERPROFILE"], "%USERPROFILE%") ) ) with open(os.path.join(BLOGGER_CLI_BIN, "blogger"), "w") as f: f.write(BIN) if not WINDOWS: # Making the file executable st = os.stat(os.path.join(BLOGGER_CLI_BIN, "blogger")) os.chmod( os.path.join(BLOGGER_CLI_BIN, "blogger"), st.st_mode | stat.S_IEXEC ) def make_env(self): if WINDOWS: return with open(os.path.join(BLOGGER_CLI_HOME, "env"), "w") as f: f.write(self.get_export_string()) def update_path(self): """ Tries to update the $PATH automatically. """ if WINDOWS: return self.add_to_windows_path() # Updating any profile we can on UNIX systems export_string = self.get_export_string() self.linux_addition = "\n{}\n".format(export_string) updated = [] profiles = self.get_unix_profiles() for profile in profiles: if not os.path.exists(profile): continue with open(profile, "r") as f: content = f.read() if self.linux_addition not in content: with open(profile, "a") as f: f.write(self.linux_addition) updated.append(os.path.relpath(profile, HOME)) def add_to_windows_path(self): try: old_path = self.get_windows_path_var() except WindowsError: old_path = None if old_path is None: print( colorize( "warning", "Unable to get the PATH value. It will not be updated automatically", ) ) self._modify_path = False return new_path = BLOGGER_CLI_BIN if BLOGGER_CLI_BIN in old_path: old_path = old_path.replace(BLOGGER_CLI_BIN + ";", "") if old_path: new_path += ";" new_path += old_path self.set_windows_path_var(new_path) def get_windows_path_var(self): with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root: with winreg.OpenKey(root, "Environment", 0, winreg.KEY_ALL_ACCESS) as key: path, _ = winreg.QueryValueEx(key, "PATH") return path def set_windows_path_var(self, value): import ctypes with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root: with winreg.OpenKey(root, "Environment", 0, winreg.KEY_ALL_ACCESS) as key: winreg.SetValueEx(key, "PATH", 0, winreg.REG_EXPAND_SZ, value) # Tell other processes to update their environment HWND_BROADCAST = 0xFFFF WM_SETTINGCHANGE = 0x1A SMTO_ABORTIFHUNG = 0x0002 result = ctypes.c_long() SendMessageTimeoutW = ctypes.windll.user32.SendMessageTimeoutW SendMessageTimeoutW( HWND_BROADCAST, WM_SETTINGCHANGE, 0, u"Environment", SMTO_ABORTIFHUNG, 5000, ctypes.byref(result), ) def remove_from_path(self): if WINDOWS: return self.remove_from_windows_path() return self.remove_from_unix_path() def remove_from_windows_path(self): path = self.get_windows_path_var() blogger_cli_path = BLOGGER_CLI_BIN if blogger_cli_path in path: path = path.replace(BLOGGER_CLI_BIN + ";", "") if blogger_cli_path in path: path = path.replace(BLOGGER_CLI_BIN, "") self.set_windows_path_var(path) def remove_from_unix_path(self): # Updating any profile we can on UNIX systems export_string = self.get_export_string() addition = "{}\n".format(export_string) profiles = self.get_unix_profiles() for profile in profiles: if not os.path.exists(profile): continue with open(profile, "r") as f: content = f.readlines() if addition not in content: continue new_content = [] for line in content: if line == addition: if new_content and not new_content[-1].strip(): new_content = new_content[:-1] continue new_content.append(line) with open(profile, "w") as f: f.writelines(new_content) def get_export_string(self): path = BLOGGER_CLI_BIN.replace(os.getenv("HOME", ""), "$HOME") export_string = 'export PATH="{}:$PATH"'.format(path) return export_string def get_unix_profiles(self): profiles = [os.path.join(HOME, ".profile")] shell = os.getenv("SHELL", "") if "zsh" in shell: zdotdir = os.getenv("ZDOTDIR", HOME) profiles.append(os.path.join(zdotdir, ".zprofile")) bash_profile = os.path.join(HOME, ".bash_profile") if os.path.exists(bash_profile): profiles.append(bash_profile) return profiles def display_pre_message(self): if WINDOWS: home = BLOGGER_CLI_BIN.replace( os.getenv("USERPROFILE", ""), "%USERPROFILE%" ) else: home = BLOGGER_CLI_BIN.replace(os.getenv("HOME", ""), "$HOME") kwargs = { "blogger-cli": colorize("info", "blogger-cli"), "blogger_cli_home_bin": colorize("comment", home), } if not self._modify_path: kwargs["platform_msg"] = PRE_MESSAGE_NO_MODIFY_PATH else: if WINDOWS: kwargs["platform_msg"] = PRE_MESSAGE_WINDOWS else: profiles = [ colorize("comment", p.replace(os.getenv("HOME", ""), "$HOME")) for p in self.get_unix_profiles() ] kwargs["platform_msg"] = PRE_MESSAGE_UNIX.format( rcfiles="\n".join(profiles), plural="s" if len(profiles) > 1 else "" ) print(PRE_MESSAGE.format(**kwargs)) def display_pre_uninstall_message(self): home_bin = BLOGGER_CLI_BIN if WINDOWS: home_bin = home_bin.replace(os.getenv("USERPROFILE", ""), "%USERPROFILE%") else: home_bin = home_bin.replace(os.getenv("HOME", ""), "$HOME") kwargs = { "blogger-cli": colorize("info", "blogger-cli"), "blogger_cli_home_bin": colorize("comment", home_bin), } print(PRE_UNINSTALL_MESSAGE.format(**kwargs)) def display_post_message(self, version): print("") kwargs = { "blogger-cli": colorize("info", "blogger-cli"), "version": colorize("comment", version), } if WINDOWS: message = POST_MESSAGE_WINDOWS if not self._modify_path: message = POST_MESSAGE_WINDOWS_NO_MODIFY_PATH blogger_cli_home_bin = BLOGGER_CLI_BIN.replace( os.getenv("USERPROFILE", ""), "%USERPROFILE%" ) else: message = POST_MESSAGE_UNIX blogger_cli_home_bin = BLOGGER_CLI_BIN.replace( os.getenv("HOME", ""), "$HOME" ) kwargs["blogger_cli_home_env"] = colorize( "comment", BLOGGER_CLI_ENV.replace(os.getenv("HOME", ""), "$HOME") ) kwargs["linux_addition"] = self.linux_addition kwargs["blogger_cli_home_bin"] = colorize("comment", blogger_cli_home_bin) print(message.format(**kwargs)) def call(self, *args): return subprocess.check_output(args, stderr=subprocess.STDOUT) def _get(self, url): request = Request(url, headers={"User-Agent": "Python Blogger-cli"}) with closing(urlopen(request)) as r: return r.read() def main(): parser = argparse.ArgumentParser( description="Installs the latest (or given) version of blogger-cli" ) parser.add_argument("--version", dest="version") parser.add_argument( "-f", "--force", dest="force", action="store_true", default=False ) parser.add_argument( "-y", "--yes", dest="accept_all", action="store_true", default=False ) parser.add_argument( "--uninstall", dest="uninstall", action="store_true", default=False ) args = parser.parse_args() installer = Installer( version=args.version or os.getenv("BLOGGER_CLI_VERSION"), force=args.force, accept_all=args.accept_all or string_to_bool(os.getenv("BLOGGER_CLI_ACCEPT", "0")) or not is_interactive(), ) if args.uninstall or string_to_bool(os.getenv("BLOGGER_CLI_UNINSTALL", "0")): return installer.uninstall() return installer.run() if __name__ == "__main__": sys.exit(main())