#!/usr/bin/env python import argparse import contextlib import imp import os import platform import sys import tempfile import time import urllib # dynamically loaded sh = None PROJECT_NAME = "advocoders" PROJECT_GIT_REPO = "git@github.com:chase-seibert/advocoders.git" HOST_NAMES = "advocoders.localhost.com" @contextlib.contextmanager def make_temp_directory(): temp_dir = tempfile.mkdtemp() os.chdir(temp_dir) yield temp_dir def get_options(): home_dir = os.path.expanduser("~") working_directory = os.getcwd() return { "home_dir": home_dir, "working_directory": working_directory, "project_dir": os.path.join(home_dir, "projects", PROJECT_NAME), } def load_this_script_deps(): ''' This is tricky; we can't assume that pip or easy_install is already installed. Just download what you need manually and import it. ''' global sh try: import sh as _sh sh = _sh except ImportError: urllib.urlretrieve("https://raw.github.com/amoffat/sh/master/sh.py", "sh.py") sh = imp.load_source("sh", "sh.py") def get_os_and_version(): if sys.platform == "darwin": return "osx", float('.'.join(platform.mac_ver()[0].split('.')[:2])) if sys.platform == "linux2": return "linux", None # don't need anything version specific raise Exception("Unsupported OS: %s" % sys.platform) def get_regular_username(): ''' sudo preserves the orignal user in a env variable ''' return os.environ.get('SUDO_USER') def bash_config_file(options): _os = options.get('os') bash_profiles = { "linux": ".bashrc", "osx": ".bash_profile", } if _os in bash_profiles: return os.path.join(options.get("home_dir"), bash_profiles.get(_os)) raise NotImplementedError() def prepend_file(file_path, content): with file(file_path, 'r') as original: original_contents = original.read() with file(file_path, 'w') as modified: modified.write(content + '\n') modified.write(original_contents) class InstallStep(object): root = False def __init__(self, options): self.options = options self.verbose_kwargs = {} if not self.options.get('quiet'): self.verbose_kwargs = dict(_out=sys.stdout, _err=sys.stdout) # for running commands as non-root user if self.root: self.sh = sh else: self.sh = sh.sudo.bake("-E", "-u", get_regular_username()) def run_virtualenv(self, command, *args, **kwargs): ''' can't rely on sh.pip or sh.python to use the virtualenv version ''' command_bin = sh.which(command) return sh.sudo("-E", "-u", get_regular_username(), command_bin, *args, **kwargs) @property def display_name(self): return self.__class__.__name__ @property def display_doing(self): return self.__doc__.strip() @property def already_satisfied(self): return False def setup(self): raise NotImplementedError() def always_run(self): pass @property def display_troubleshooting(self): return None class InstallStepAsRoot(InstallStep): root = True class XCodeInstall(InstallStepAsRoot): ''' Downloading and installing XCode command line tools ''' @property def already_satisfied(self): try: return len(self.sh.make("-version")) > 0 except: return False def setup(self): version = self.options.get("version") # see: https://devimages.apple.com.edgekey.net/downloads/xcode/simulators/index-3905972D-B609-49CE-8D06-51ADC78E07BC.dvtdownloadableindex downloads = { 10.7: "http://devimages.apple.com/downloads/xcode/command_line_tools_for_xcode_os_x_lion_april_2013.dmg", 10.8: "http://devimages.apple.com/downloads/xcode/command_line_tools_for_xcode_os_x_mountain_lion_april_2013.dmg", } if version not in downloads: raise NotImplementedError("Could not locate XCode download for OSX %s" % version) download_file = downloads.get(version) # save this OUTSIDE the normal tmp dir; in case we need to restart install dmg_file = "/tmp/xcode.dmg" if not os.path.exists(dmg_file): urllib.urlretrieve(download_file, dmg_file) volume_dir = "/tmp/xcode" if not os.path.exists(volume_dir): self.sh.hdiutil("attach", "-mountpoint", volume_dir, dmg_file, **self.verbose_kwargs) mpkg_file = [f for f in os.listdir(volume_dir) if f.endswith(".mpkg")][0] try: self.sh.installer("-pkg", os.path.join(volume_dir, mpkg_file), "-target", "/", **self.verbose_kwargs) except sh.ErrorReturnCode as e: print e.stderr finally: self.sh.hdiutil("detach", volume_dir) @property def display_troubleshooting(self): return """ You can manually install the XCode Command Line Tools by signing up for a free Apple ID and logging into https://developer.apple.com/xcode/ Note: you don't need the full 2GB installer for Xcode; the command line tools package is "only" 200MB. """ class HomebrewInstall(InstallStep): ''' Installing homebrew ''' @property def already_satisfied(self): try: return len(self.sh.brew("--version")) > 0 except: return False def setup(self): homebrew_fix = "homebrew_fix.rb" urllib.urlretrieve("https://gist.github.com/rpavlik/768518/raw/fix_homebrew.rb", homebrew_fix) self.sh.ruby(homebrew_fix, **self.verbose_kwargs) homebrew_source = "homebrew.rb" urllib.urlretrieve("https://raw.github.com/mxcl/homebrew/go", homebrew_source) self.sh.ruby(homebrew_source, **self.verbose_kwargs) @property def display_troubleshooting(self): return """ You can install homebrew manually by running this: ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/go)" """ class BrewDependenciesInstall(InstallStep): ''' Installing dependendies ''' dependencies = [ "libxml2", "libxslt", "libmagic", "postgresql", "libevent", ] @property def already_satisfied(self): installed = [i.strip() for i in self.sh.brew("list", "-1").split("\n") if i] not_installed = list(set(self.dependencies) - set(installed)) return not_installed == [] def setup(self): self.sh.brew("install", self.dependencies, **self.verbose_kwargs) @property def display_troubleshooting(self): return """ You can install the dependencies manually by running this: brew install %s """ % (' '.join(self.dependencies)) class VirtualEnvInstall(InstallStepAsRoot): ''' Installing pip and virtualenv ''' @property def already_satisfied(self): try: return len(self.sh.virtualenv("--version")) > 0 except: return False def setup(self): self.sh.easy_install("pip", **self.verbose_kwargs) self.sh.pip("install", "virtualenv", **self.verbose_kwargs) class AptDependenciesInstall(InstallStepAsRoot): ''' Installing apt dependencies ''' dependencies = [ "libxml2-dev", "libxslt1-dev", "libmagic1", "python-virtualenv", "postgresql", "libpq-dev", "libevent-dev", ] @property def already_satisfied(self): return "0 upgraded, 0 newly installed" in self._run("--dry-run") def setup(self): self._run(**self.verbose_kwargs) def _run(self, flag="--yes", **kwargs): return self.sh.apt_get("install", flag, self.dependencies, **kwargs) @property def display_troubleshooting(self): return """ You can install the dependencies manually by running this: sudo apt-get install --yes %s """ % (' '.join(self.dependencies)) class UpdateHostsFile(InstallStepAsRoot): ''' Updating hosts file ''' @property def already_satisfied(self): try: return self.sh.grep(sh.cat("/etc/hosts"), HOST_NAMES) except sh.ErrorReturnCode: return False def setup(self): prepend_file("/etc/hosts", "127.0.0.1 %s" % HOST_NAMES) @property def display_troubleshooting(self): return """ You can update your hosts file manually with: sudo sh -c 'echo "127.0.0.1 %s" >> /etc/hosts' """ % HOST_NAMES class GitClone(InstallStep): ''' Cloning the project repo ''' @property def already_satisfied(self): return os.path.exists(self.options.get("project_dir")) def setup(self): self.sh.git("clone", PROJECT_GIT_REPO, self.options.get("project_dir")) class VirtualEnv(InstallStep): ''' Setting up a virtualenv and installing python dependencies ''' def __init__(self, options): os.chdir(options.get("project_dir")) self.activate_this = os.path.join(options.get("project_dir"), 'virtualenv/bin/activate_this.py') self.requirements_file = os.path.join(options.get("project_dir"), "requirements.txt") super(VirtualEnv, self).__init__(options) @staticmethod def extract_pips(lines): return [line.split('==')[0].strip().lower() for line in lines] def pip(self, *args, **kwargs): return self.run_virtualenv("pip", *args, **kwargs) @property def already_satisfied(self): # this is the equivalent of source virtualenv/bin/activate, but # source is not usable via sh.py if not os.path.exists(self.activate_this): return False self.activate() items_installed = VirtualEnv.extract_pips(self.pip("freeze").split('\n')) items_required = VirtualEnv.extract_pips(open(self.requirements_file).readlines()) items_diff = list(set(items_required) - set(items_installed)) return not items_diff def activate(self): execfile(self.activate_this, dict(__file__=self.activate_this)) def always_run(self): # want this to be activated for subsequent install steps if os.path.exists(self.activate_this): self.activate() def setup(self): if not os.path.exists(self.activate_this): self.sh.virtualenv("--no-site-packages", "--distribute", "virtualenv") self.activate() self.pip("install", "-r", self.requirements_file, **self.verbose_kwargs) class SetEnvironmentVariable(InstallStep): variable = None value = None @property def display_doing(self): return "Setting the %s environment variable" % self.variable @property def already_satisfied(self): try: return self.sh.grep(sh.cat(bash_config_file(self.options)), self.variable) except sh.ErrorReturnCode: return False def always_run(self): os.environ[self.variable] = self.value def setup(self): prepend_file(bash_config_file(self.options), "export %s=%s" % (self.variable, self.value)) class ProductionEnvironmentVariable(SetEnvironmentVariable): variable = "PRODUCTION" value = "False" class SetupDatabase(InstallStep): ''' Creating a local sqlite database ''' @property def already_satisfied(self): return os.path.exists(os.path.join(options.get("project_dir"), "sqlite3.db")) def setup(self): os.chdir(self.options.get("project_dir")) self.run_virtualenv("python", "manage.py", "syncdb", "--noinput", **self.verbose_kwargs) self.run_virtualenv("python", "manage.py", "migrate", "advocoders", **self.verbose_kwargs) self.run_virtualenv("python", "manage.py", "migrate", "social_auth", **self.verbose_kwargs) class RunServerAlias(InstallStep): ''' Creating the bash alias ''' @property def already_satisfied(self): try: return self.sh.grep(sh.cat(bash_config_file(self.options)), 'runserver') except sh.ErrorReturnCode: return False def setup(self): alias = 'alias runserver="cd %s; source ../virtualenv/bin/activate; ./manage.py runserver 8000"' % ( self.options.get("project_dir")) prepend_file(bash_config_file(self.options), alias) class RunServer(InstallStep): ''' Running local development web server ''' @property def already_satisfied(self): return True def _open(self, web_address): _os = self.options.get('os') open_commands = { "linux": "xdg-open", "osx": "open", } if _os in open_commands: getattr(self.sh, open_commands.get(_os))(web_address) def always_run(self): os.chdir(self.options.get("project_dir")) runserver = self.run_virtualenv("python", "manage.py", "runserver", "0:8000", _bg=True, _iter=True, _out=sys.stdout, _err=sys.stderr) server_address = "http://localhost:8000/" print """ =================================================== Congradulations! Your local development environment is successfully setup and live at %s. Note: you can run the server directly in a NEW terminal by typing "runserver". =================================================== """ % (server_address) time.sleep(3) self._open(server_address) try: runserver.wait() except KeyboardInterrupt: pass def get_steps(_os, version): if _os == "osx": yield XCodeInstall yield HomebrewInstall yield BrewDependenciesInstall yield VirtualEnvInstall if _os == "linux": yield AptDependenciesInstall yield UpdateHostsFile yield GitClone yield VirtualEnv yield ProductionEnvironmentVariable yield SetupDatabase yield RunServerAlias yield RunServer def run_steps(temp_dir, options): os_version_tuple = get_os_and_version() options['os'] = os_version_tuple[0] options['version'] = os_version_tuple[1] for step_cls in get_steps(*os_version_tuple): step = step_cls(options) step.always_run() if step.display_name in options.get('skip'): continue if step.already_satisfied and step.display_name not in options.get('force'): continue print '%s...' % step.display_doing try: step.setup() except sh.ErrorReturnCode as e: print e.stderr if step.display_name in options.get('force'): # when we force, we likely want to trouble-shoot just that step exit(1) if step.already_satisfied: continue print "ERROR: %s failed." % step.display_name if step.display_troubleshooting: print print "=================== Trouble-shooting ===================" print step.display_troubleshooting exit(1) if __name__ == "__main__": # sudo -E python scripts/devlocal_setup.py parser = argparse.ArgumentParser(description='Automated devlocal setup') parser.add_argument('--force', default='', help='Run the specified steps again, comma separated.') parser.add_argument('--skip', default='', help='Skip the specified steps, comma separated.') parser.add_argument('--quiet', action="store_true", help='No verbose logging') args = parser.parse_args() options = get_options() options.update(vars(args)) with make_temp_directory() as temp_dir: load_this_script_deps() # must be in temp dir, downloads files sh.chmod("777", temp_dir) # git clone needs to be able to swtich back run_steps(temp_dir, options)