def install_zsh(): url = "https://nchc.dl.sourceforge.net/project/zsh/zsh/5.7.1/zsh-5.7.1.tar.xz" fname = "zsh-5.7.1.tar.xz" download(url, fname) execute(f"tar xvaf {fname}") mkdir("app/") execute(f"cd zsh-5.7.1 && ./configure --prefix={ZSH_DIR} && make install")
def pull_submodules(filter_path): from homely.ui import head head("Pulling latest submodules under {}/".format(filter_path)) # git a list of git submodules _, stdout, _ = execute(['git', 'submodule', 'status', filter_path], cwd=HERE, stdout=True) paths = [ line[1:].split(' ')[1] for line in stdout.decode('utf-8').splitlines() ] for path in paths: cmd = [ 'git', 'submodule', 'update', '--remote', '--recursive', '--', path ] execute(cmd, cwd=HERE) # check whether submodules are changed _, stdout, _ = execute(['git', 'status', '--short', filter_path], cwd=HERE, stdout=True) if stdout.strip(): execute(['git', 'add', filter_path], cwd=HERE) execute([ 'git', 'commit', '-m', 'Automated update of submodules under {}'.format(filter_path) ], cwd=HERE) # then we need to follow up with a 'git submodule update --recursive' in # case the submodules have their own submodules that we accidentally # fast-forwarded for path in paths: cmd = ['git', 'submodule', 'update', '--recursive', '--', path] execute(cmd, cwd=HERE)
def makechanges(self): # try each method for method in _METHODS: localname = self._methods.get(method, self._name) if localname is False: continue def getdefaultcmd(name): return [method, 'install', name] cmd = _INSTALL.get(method, getdefaultcmd)(localname) # see if the required executable is installed if not haveexecutable(cmd[0]): continue if method in _ASROOT: if not allowinteractive(): raise HelperError("Need to be able to escalate to root") cmd.insert(0, 'sudo') execute(cmd) # record the fact that we installed this thing ourselves factname = 'InstalledPackage:%s:%s' % (method, localname) self._setfact(factname, True) return raise HelperError("No way to install %s" % self._name)
def makechanges(self): # try each method for method in _METHODS: localname = self._methods.get(method, self._name) if localname is False: continue def getdefaultcmd(name): return [method, 'install', name] cmd = _INSTALL.get(method, getdefaultcmd)(localname) # see if the required executable is installed if not haveexecutable(cmd[0]): continue if not _ALLOW_INSTALL: raise HelperError( "InstallPackage() is not allowed to install packages" ", as per setallowinstall()") if method in _ASROOT: if not allowinteractive(): raise HelperError("Need to be able to escalate to root") cmd.insert(0, 'sudo') execute(cmd) # record the fact that we installed this thing ourselves factname = 'InstalledPackage:%s:%s' % (method, localname) self._setfact(factname, True) return raise HelperError("No way to install %s" % self._name)
def osx(): execute([ 'defaults', 'write', 'NSGlobalDomain', 'InitialKeyRepeat', '-int', '15' ]) # KeyRepeat < 1.0 doesn't work :-( execute( ['defaults', 'write', 'NSGlobalDomain', 'KeyRepeat', '-float', '1.0'])
def brew_install(): if haveexecutable('brew'): return if yesno('install_homebrew', 'Install Homebrew?', default=True): install_cmd = '/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"' execute(['bash', '-c', install_cmd], stdout="TTY")
def poetry_install(): execute( [ 'sh', '-c', 'curl -sSL https://install.python-poetry.org | python3 -' ], stdout="TTY", )
def nvim_ls_ts(): execute(['npm', 'install'], cwd=HERE + '/nvim_ts') # We need these two to be available globally as they probably won't exist in project packages. # The language server will also need 'prettier', 'typescript' and 'eslint' # packages, however those are more likely to be added to the project directly. symlink('nvim_ts/node_modules/.bin/typescript-language-server', '~/bin/typescript-language-server') symlink('nvim_ts/node_modules/.bin/eslint_d', '~/bin/eslint_d')
def install_php_cs_fixer(): if not yesno('want_php_cs_fixer', 'Install php-cs-fixer?'): return init_composer() execute( ['composer', 'global', 'require', 'friendsofphp/php-cs-fixer'], )
def git_install(): if not yesno('upgrade_git', 'Install latest git from ppa:git-core/ppa?', default=want_full): return for cmd in [ ['sudo', 'apt-add-repository', '-y', 'ppa:git-core/ppa'], ['sudo', 'apt-get', 'update'], ['sudo', 'apt-get', 'install', 'git'], ]: execute(cmd, stdout="TTY")
def install_php_language_server(): if not yesno('want_php_langserver', 'Install PHP Language Server?'): return init_composer() execute( ['composer', 'global', 'require', 'felixfbecker/language-server'], ) execute( ['composer', 'run-script', 'parse-stubs'], cwd=HOME + '/.config/composer/vendor/felixfbecker/language-server', )
def makechanges(self): cmd = [ self._pipcmd, 'uninstall', self._name, '--disable-pip-version-check', '--yes', ] factname = 'pipinstall:%s:%s' % (self._pipcmd, self._name) try: execute(cmd) finally: self._clearfact(factname) return []
def font_install(): fonts = [ 'homebrew/cask-fonts/font-inconsolata', # this download doesn't seem to work any more # 'homebrew/cask-fonts/font-anonymous-pro', ] if wantpowerline(): fonts.extend([ 'homebrew/cask-fonts/font-inconsolata-for-powerline', # this one seems to require `brew install svn` which I'm maybe not prepared to do # 'homebrew/cask-fonts/font-anonymice-powerline', ]) # install some nicer fonts execute(['brew', 'install'] + fonts)
def ubuntu_key_repeat_rate(): if not yesno('ubuntu_set_repeat_rate', 'Ubuntu: Set keyboard repeat rate?'): return new_values = [ ('repeat-interval', 'uint32 15'), ('delay', 'uint32 210'), ] for key, value in new_values: execute([ 'gsettings', 'set', 'org.gnome.desktop.peripherals.keyboard', key, value ])
def ubuntu_app_switcher_current_workspace(): if not yesno( 'ubuntu_set_app_switcher_current_workspace', 'Ubuntu: Set alt-tab to only use current workspace?', ): return execute([ 'gsettings', 'set', 'org.gnome.shell.app-switcher', 'current-workspace-only', 'true', ])
def vim_plugin_update(): if allowinteractive(): execute(['vim', '+PlugClean', '+PlugUpdate'], stdout="TTY") if HOMELY.wantnvim(): execute(['nvim', '+PlugClean', '+PlugUpdate'], stdout="TTY") return # install the self-updating plugins now if True: template = '#!/usr/bin/env bash\nvim-update-then-run {} "$@"\n' for what in ('vim', 'nvim'): exec_ = HOME + '/bin/' + what with writefile(exec_) as f: f.write(template.format(what)) os.chmod(exec_, 0o755)
def makechanges(self): if self._pipcmd is None: raise HelperError("%s executable not found" % self._pip) cmd = [ self._pipcmd, 'install', self._name, '--user', '--disable-pip-version-check', ] if self._scripts is not None: cmd.append('--install-option=--install-scripts=%s' % self._scripts) execute(cmd) factname = 'pipinstall:%s:%s' % (self._pipcmd, self._name) self._setfact(factname, True)
def isdone(self): if not os.path.exists(self._real_clone_to): return False if self._tag: # has the correct branch or tag been checked out? current = execute(['git', 'tag', '--points-at', 'HEAD'], cwd=self._real_clone_to, stdout=True)[1] if self._tag not in map(str, current.splitlines()): return False # if there's no symlinks, we can't tell if it's done or not # TODO: test this before releasing if not len(self._symlinks): return False # do the symlinks exist? for target, linkname in self._symlinks: if not os.path.islink(linkname): return False if os.readlink(linkname) != target: return False # it appears to be done ... yay return True
def isdirty(self): cmd = ['git', 'status', '--porcelain'] out = execute(cmd, cwd=self.repo_path, stdout=True)[1] for line in out.split(b'\n'): if len(line) and not line.startswith(b'?? '): return True return False
def getpippaths(): if IS_OSX: return {} # do we need to out a pip config such that py2/py3 binaries don't clobber each other? question = 'Force pip to install into separate py2/py3 bin dirs?' if not yesno('force_pip_bin_paths', question, None): return {} scripts = {} for version in (2, 3): # TODO: we probably should drop into vim somewhere and make sure g:my_pyX_paths is # defined in prefs.vim or else our stuff is gonna be broken # TODO: we also want these in our $PATH ... or not? pip = "pip%d" % version var = "g:my_py%d_paths" % version if not haveexecutable(pip): continue stdout = execute([pip, '--version'], stdout=True)[1].decode('utf-8').rstrip() assert re.search(r' \(python \d+\.\d+\)$', stdout) version = stdout.rsplit(' ', 1)[1][:-1] path = '%s/.local/python-%s-bin' % (HOME, version) scripts[pip] = path lineinfile('~/.vimrc', "let %s = ['%s']" % (var, path), where=WHERE_END) return scripts
def install_fedora_copr(): if not allow_installing_stuff: return False if not haveexecutable('yum'): return False copr_url = 'https://copr.fedorainfracloud.org/coprs/carlwgeorge/ripgrep/repo/epel-7/carlwgeorge-ripgrep-epel-7.repo' if not yesno('allow_fedora_copr', 'Add fedora COPR repo on this host?', None): return False # enable the repo installpkg('yum-utils') execute(['sudo', 'yum-config-manager', '--add-repo=' + copr_url], stdout="TTY") return True
def fzf_install(): if not yesno('install_fzf', 'Install fzf?', want_full): return if haveexecutable('brew') and allow_installing_stuff: installpkg('fzf') brewpath = execute(['brew', '--prefix'], stdout=True)[1].decode('utf-8').strip() if brewpath == '/opt/homebrew': # brew puts the fzf files into versioned folders, so all we can do # is glob and sort (which isn't perfect because it would need to be # a semver-compatible sort) and pick the first one fzf_path = execute( [ 'bash', '-c', f'echo {brewpath}/Cellar/fzf/* | sort -r | head -n 1' ], stdout=True, )[1].decode('utf-8').strip() else: # this is how it was on my old mac fzf_path = brewpath + '/opt/fzf' else: # do it the long way import os.path fzf_repo = os.path.expanduser('~/src/fzf.git') fzf_install = InstallFromSource('https://github.com/junegunn/fzf.git', fzf_repo) fzf_install.select_tag('0.17.3') fzf_install.compile_cmd([ ['./install', '--bin'], ]) fzf_install.symlink('bin/fzf', '~/bin/fzf') run(fzf_install) execute(['./install', '--bin'], cwd=fzf_repo, stdout='TTY') fzf_path = fzf_repo lineinfile('~/.bashrc', 'source {}/shell/completion.bash'.format(fzf_path)) lineinfile('~/.bashrc', 'source {}/shell/key-bindings.bash'.format(fzf_path)) if wantzsh(): lineinfile('~/.zshrc', 'source {}/shell/completion.zsh'.format(fzf_path)) lineinfile('~/.zshrc', 'source {}/shell/key-bindings.zsh'.format(fzf_path))
def iterm2_prefs(): if yesno('use_iterm2_prefs', 'Use custom iterm2 prefs?', default=True): execute([ 'defaults', 'write', 'com.googlecode.iterm2.plist', 'PrefsCustomFolder', '-string', HERE + '/iterm2', ]) execute([ 'defaults', 'write', 'com.googlecode.iterm2.plist', 'LoadPrefsFromCustomFolder', '-bool', 'true', ])
def pypirc(): rc = HOME + '/.pypirc' if not yesno('write_pypirc', 'Write a .pypirc file?', want_full): return if not os.path.exists(rc): with open(rc, 'w') as f: f.write('[distutils]\n') f.write('index-servers=pypi\n') f.write('\n') f.write('[pypi]\n') f.write('repository = https://upload.pypi.org/legacy/\n') f.write('# TODO: put your real username here\n') f.write('username = USERNAME\n') f.write('# TODO: put your real password here\n') f.write('password = PASSWORD\n') with open(rc) as f: if 'TODO' in f.read() and yesno( None, "Edit %s now?" % rc, True, noprompt=False): execute(['vim', rc], stdout="TTY") execute(['chmod', '600', rc])
def ubuntu_swap_caps_escape(): if not yesno('ubuntu_swap_caps_escape', 'Ubuntu: Swap caps/escape using dconf-editor?'): return cmd = ['dconf', 'read', '/org/gnome/desktop/input-sources/xkb-options'] current = execute(cmd, stdout=True)[1].strip() if b"'caps:swapescape'" in current: # already done return if current == b'' or current == b'[]': new_value = "['caps:swapescape']" else: raise Exception( "TODO: don't know how to modify xkb-options value") # noqa execute([ 'dconf', 'write', '/org/gnome/desktop/input-sources/xkb-options', new_value ])
def venv_exec(venv_pip, cmd, **kwargs): import shlex env = kwargs.pop('env', None) if env is None: env = os.environ env.pop('__PYVENV_LAUNCHER__', None) activate = os.path.dirname(venv_pip) + '/activate' cmd = [ 'bash', '-c', 'source {} && {}'.format(activate, " ".join(map(shlex.quote, cmd))) ] return execute(cmd, env=env, **kwargs)
def main(): # TODO to auto rename to an "x.old" and warn? files.symlink("bashrc", ".bashrc") files.symlink("bash_profile", ".bash_profile") files.symlink("profile", ".profile") # TODO maybe add lines instead? files.symlink("editorconfig", ".editorconfig") if system.haveexecutable("task"): files.symlink("taskrc", ".taskrc") if system.haveexecutable("asdf"): files.symlink("tool-versions", ".tool-versions") files.symlink("asdfrc", ".asdfrc") files.symlink("default-golang-pkgs", ".default-golang-pkgs") # git common config: # TODO error if git not present # TODO into separate function gitpath = os.path.join(HERE, "common.gitconfig") system.execute(["git", "config", "--global", "include.path", gitpath]) # NOTE this might overwrite if there's another include.path already present # TODO revert/cleanup somehow? # TODO check that path specification using ~/ works # TODO does pipinstall use system pip or HOMELY's one? files.symlink("vimrc", ".vimrc") # install vim-plug files.mkdir(".vim") files.mkdir(".vim/autoload") files.mkdir(".vim/swaps") files.mkdir(".vim/backups") files.mkdir(".vim/doc") files.download( "https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim", "~/.vim/autoload/plug.vim", ) # link custom ftplugin files files.symlink("vim-ftplugin", ".vim/ftplugin") if system.haveexecutable("pipenv"): # TODO warn that they need to reload for this to take effect files.lineinfile(".bashrc.local", 'eval "$(pipenv --completion)"') if system.haveexecutable("i3"): # ~/.local/bin is towards the front of PATH thanks to .profile files.symlink("i3exit.sh", "~/.local/bin/i3exit") files.symlink("locker.sh", "~/.local/bin/locker") files.symlink("i3.conf", "~/.i3/config") if system.haveexecutable("kitty"): files.mkdir("$XDG_CONFIG_HOME/kitty") files.symlink("kitty.conf", "$XDG_CONFIG_HOME/kitty/kitty.conf") if system.haveexecutable("pre-commit"): # Install pre-commit in all new/cloned repos # See also https://pre-commit.com/#pre-commit-init-templatedir template_path = "{}/.git-template".format(os.environ["HOME"]) system.execute(["git", "config", "--global", "init.templateDir", template_path]) system.execute(["pre-commit", "init-templatedir", template_path]) return 0
def makechanges(self): # look for any of the facts saying we installed these things for method in _METHODS: localname = self._methods.get(method, self._name) factname = 'InstalledPackage:%s:%s' % (method, localname) if not self._getfact(factname, False): continue def defaultuninstall(name): return [method, 'uninstall', name] cmd = _UNINSTALL.get(method, defaultuninstall)(localname) if method in _ASROOT: if not allowinteractive(): raise HelperError("Need to be able to escalate to root") cmd.insert(0, 'sudo') try: execute(cmd) finally: # always clear the fact self._clearfact(factname) raise HelperError("Didn't remove package %s" % self._name)
def _bashprofile(): if os.path.islink(bash_profile): return if os.path.exists(bash_profile): if not allowinteractive(): warn("%s needs manual review" % bash_profile) return msg = ('Move the contents of ~/.bash_profile into other files, and' ' then delete the file when you are done') cmd = ['vim', bash_profile, '+top new', '+normal! I{}'.format(msg), '+normal! gql', ] execute(cmd, stdout="TTY") if os.path.exists(bash_profile): if os.stat(bash_profile).st_size > 1: warn("{} still contains data".format(bash_profile)) return os.unlink(bash_profile) with note("Creating symlink {} -> {}".format(bash_profile, bashrc)): os.symlink(bashrc, bash_profile)
def _haspkg(pipcmd, name): cmd = [ pipcmd, 'list', '--disable-pip-version-check', ] if _needs_format(pipcmd): cmd.append('--format=legacy') output = execute(cmd, stdout=True)[1] find = '%s ' % name for line in output.decode('utf-8').split("\n"): if line.startswith(find): return True return False
def _haspkg(pipcmd, name): cmd = [ pipcmd, 'list', '--disable-pip-version-check', ] if _needs_format(pipcmd): cmd.append('--format=freeze') output = execute(cmd, stdout=True)[1] find = '%s==' % name for line in output.decode('utf-8').split("\n"): if line.startswith(find): return True return False
def tools(): if yesno('install_with', 'Install `with` utility?', want_full): withutil = InstallFromSource('https://github.com/mchav/with', '~/src/with.git') withutil.symlink('with', '~/bin/with') withutil.select_branch('master') run(withutil) if yesno('install_universal_ctags', 'Install Universal Ctags?', want_full): need_installpkg(apt=('autoconf', 'g++')) mkdir('~/bin') if haveexecutable('brew'): # install with homebrew execute(['brew', 'tap', 'universal-ctags/universal-ctags']) execute(['brew', 'install', '--HEAD', 'universal-ctags']) else: uc = InstallFromSource('https://github.com/universal-ctags/ctags', '~/src/universal-ctags.git') uc.select_branch('master') uc.compile_cmd([ ['./autogen.sh'], ['./configure'], ['make'], ]) uc.symlink('ctags', '~/bin/ctags') run(uc) elif allow_installing_stuff and yesno('install_ctags', 'Install `ctags`?', want_full): installpkg('ctags') if allow_installing_stuff and yesno('install_patch', 'Install patch?', want_full): installpkg('patch') if allow_installing_stuff and yesno('install_tidy', 'Install tidy cli tool?', want_full): installpkg('tidy') # on OSX we want to install gnu utils (brew install coreutils findutils) # and put /usr/local/opt/coreutils/libexec/gnubin in PATH if IS_OSX and haveexecutable('brew') and allow_installing_stuff: if yesno('brew_install_coreutils', 'Install gnu utils?', default=want_full): brew_list = set( execute(['brew', 'list'], stdout=True)[1].decode('utf-8').splitlines()) install = [ pkg for pkg in ('coreutils', 'findutils') if pkg not in brew_list ] if len(install): execute(['brew', 'install'] + install)
def pullchanges(self): assert not self.isremote cmd = ['git', 'pull'] code, _, err = execute(cmd, cwd=self.repo_path, stderr=True, expectexit=(0, 1)) if code == 0: return assert code == 1 needle = b'fatal: Could not read from remote repository.' if needle in err: raise ConnectionError() raise SystemError("Unexpected output from 'git pull': {}".format(err))
def clonetopath(self, dest): origin = self.repo_path execute(['git', 'clone', origin, dest])
def makechanges(self): assert self._source_repo is not None assert self._clone_to is not None if not os.path.exists(self._real_clone_to): note("Cloning %s" % self._source_repo) pull_needed = False execute(['git', 'clone', self._source_repo, self._real_clone_to]) else: pull_needed = True if not os.path.exists(os.path.join(self._real_clone_to, '.git')): raise HelperError("%s is not a git repo" % self._real_clone_to) # do we want a particular branch? if self._branch: execute(['git', 'checkout', self._branch], cwd=self._real_clone_to) if pull_needed and allowpull(): note("Updating %s from %s" % (self._clone_to, self._source_repo)) execute(['git', 'pull'], cwd=self._real_clone_to) # check the branch fact to see if we need to compile again factname = self._branchfact else: assert self._tag is not None if pull_needed and allowpull(): note("Updating %s from %s" % (self._clone_to, self._source_repo)) # NOTE: we use --force for projects like neovim that have a # rolling 'nightly' tag execute(['git', 'fetch', '--tags', '--force'], cwd=self._real_clone_to) execute(['git', 'checkout', self._tag], cwd=self._real_clone_to) # if we used a tag name, create a 'fact' to prevent us re-compiling # each time we run factname = '{}:compile-tag:{}:{}'.format( self.__class__.__name__, self._real_clone_to, self._tag) docompile = False if self._compile: last_compile, prev_cmds = self._getfact(factname, (0, None)) what = ("Branch {}".format(self._branch) if self._branch else "Tag {}".format(self._tag)) if last_compile == 0: note("{} has never been compiled".format(what)) docompile = True elif (self._expiry is not None and ((last_compile + self._expiry) < time.time())): note("{} is due to be compiled again".format(what)) docompile = True elif prev_cmds != self._compile: note("{} needs to be compiled again with new commands" .format(what)) docompile = True # run any compilation commands if docompile: # FIXME: we probably need to delete all the symlink targets before # compiling, as this is our best way of determining that the # compilation has failed ... stdout = "TTY" if self._needs_tty else None for cmd in self._compile: if cmd[0] == "sudo" and not _ALLOW_INSTALL: raise HelperError( "%s is not allowed to run commands as root" ", as per setallowinstall()") execute(cmd, cwd=self._real_clone_to, stdout=stdout) self._setfact(factname, (time.time(), self._compile)) # create new symlinks for source, dest in self._symlinks: with note("Ensure symlink exists: %s -> %s" % (source, dest)): if os.path.islink(dest): target = os.readlink(dest) if os.path.realpath(target) != os.path.realpath(source): raise HelperError("Symlink %s is not pointing at %s" % (dest, source)) continue if os.path.exists(dest): raise HelperError("%s already exists" % dest) os.symlink(source, dest)