Mercurial > dotfiles
view install.py @ 508:5dcfe74f0465
Alias for tmux with nice colors.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Fri, 12 Nov 2021 12:01:50 -0800 |
parents | 76defcf6bf02 |
children | 38aa9895725d |
line wrap: on
line source
import os import os.path import sys import stat import shutil import argparse import functools import traceback import subprocess import configparser # Utility stuff. dotfiles_dir = os.path.abspath(os.path.dirname(__file__)) is_nix = True is_mac = False is_windows = False if sys.platform == "win32": is_nix = False is_windows = True if sys.platform == 'darwin': is_mac = True def _is_executable(fpath): return os.path.isfile(fpath) and os.access(fpath, os.X_OK) def which(exename): for path in os.environ.get("PATH", "").split(os.pathsep): exepath = os.path.join(path, exename) if _is_executable(exepath): return exepath return None def _p(*paths, force_unix=False): res = os.path.join(dotfiles_dir, *paths) if force_unix: res = res.replace('\\', '/') else: res = res.replace('/', os.sep) return res def nixslash(path): return path.replace('\\', '/') def ensure_dir(path): full_path = os.path.abspath(os.path.expanduser(path)) if not os.path.isdir(full_path): os.makedirs(full_path, mode=0o700) def mklink(orig_rel_path, link_path, mode=None): orig_full_path = os.path.join(dotfiles_dir, orig_rel_path) link_full_path = os.path.abspath(os.path.expanduser(link_path)) if os.path.islink(link_full_path): print("Unlinking %s" % link_full_path) os.unlink(link_full_path) elif os.path.exists(link_full_path): print("Removing %s" % link_full_path) os.remove(link_full_path) print("%s -> %s" % (link_full_path, orig_full_path)) if not is_windows or os.path.isfile(orig_full_path): os.symlink(orig_full_path, link_full_path) else: subprocess.check_call(['mklink', '/J', link_full_path, orig_full_path], shell=True) if mode is not None: os.chmod(link_full_path, mode) def writelines(path, lines): full_path = os.path.abspath(os.path.expanduser(path)) print("%d lines to %s" % (len(lines), full_path)) with open(full_path, 'w') as fp: for l in lines: fp.write(l) fp.write('\n') def _on_rmtree_err(func, name, excinfo): os.chmod(name, stat.S_IWUSR | stat.S_IWGRP) os.remove(name) def rmtree(dirpath): shutil.rmtree(dirpath, onerror=_on_rmtree_err) # Installer method decorators. def only_on_nix(f): @functools.wraps(f) def decorator(*args, **kwargs): if is_nix: return f(*args, **kwargs) return decorator def only_on_mac(f): @functools.wraps(f) def decorator(*args, **kwargs): if is_mac: return f(*args, **kwargs) return decorator def only_on_win(f): @functools.wraps(f) def decorator(*args, **kwargs): if is_windows: return f(*args, **kwargs) return decorator def supports_forcing(f): f.__dotfiles_supports_forcing__ = True return f def needs_config(f): f.__dotfiles_needs_config__ = True return f def run_priority(prio): def wrapper(f): f.__dotfiles_priority__ = prio return f return wrapper # Installer methods. @only_on_nix def install_bash(): mklink('bashrc/bashrc', '~/.bashrc') mklink('bashrc/bash_profile', '~/.bash_profile') @only_on_nix def install_fish(): ensure_dir('~/.config/fish') writelines('~/.config/fish/config.fish', ['source %s' % _p('fish', 'config.fish')]) def _install_vim_bundle(cfg, cfg_section_name, bundle_dir, force=False): if not cfg.has_section(cfg_section_name): return os.makedirs(bundle_dir, exist_ok=True) # Keep track of lowercase directory names because the cfg parser stores # config section names in lowercase. existing_plugins = dict([(d.lower(), d) for d in os.listdir(bundle_dir)]) for name, url in cfg.items(cfg_section_name): path = os.path.join(bundle_dir, name) if url.startswith('[local]'): pass elif url.startswith('[git]'): clone_git(url[len('[git]'):], path, force=force) else: clone_hg(url, path, force=force) print() existing_plugins.pop(name, None) for k, name in existing_plugins.items(): print("Removing plugin %s" % name) ok = input("OK? [Y/n]") if ok.lower() == "y": path = os.path.join(bundle_dir, name) rmtree(path) @needs_config @supports_forcing def install_vim(cfg, force=False): vimrc_path = '~/.vimrc' if is_windows: vimrc_path = '~/_vimrc' writelines(vimrc_path, [ 'set runtimepath+=%s' % nixslash(_p('vim')), 'source %s' % nixslash(_p('vim', 'vimrc')) ]) # Create a gvimrc file mostly to fix a bug with Scoop, which # installs a _gvimrc in the base runtime path, and it can mess # up colors and settings. gvimrc_path = '~/.gvimrc' if is_windows: gvimrc_path = '~/_gvimrc' writelines(gvimrc_path, [ 'source %s' % nixslash(_p('vim', 'gvimrc')) ]) _install_vim_bundle(cfg, 'vimbundles', _p('vim', 'bundle'), force) _install_vim_bundle(cfg, 'vimbundles:local', _p('vim', 'local'), force) @run_priority(2) # Needs to run before `fish`. def install_mercurial(): hgrc_path = '~/.hgrc' if is_windows: hgrc_path = '~/mercurial.ini' writelines(hgrc_path, [ '%%include %s' % _p('hgrc/hgrc'), '[ui]', 'ignore = %s' % _p('hgrc/hgignore'), '[subrepos]', 'git:allowed = true', '[extensions]', 'hggit = %s' % _p('lib/hg/hg-git/hggit/'), 'onsub = %s' % _p('lib/hg/onsub/onsub.py'), 'allpaths = %s' % _p('lib/hg/allpaths/mercurial_all_paths.py'), 'prompt = %s' % _p('lib/hg/hg-prompt/prompt.py'), 'evolve = %s' % _p('lib/hg/evolve/hgext3rd/evolve'), '[alias]', ('dlog = log --pager=yes --style=%s' % _p('lib/hg/mercurial-cli-templates/map-cmdline.dlog', force_unix=True)), ('slog = log --pager=yes --style=%s' % _p('lib/hg/mercurial-cli-templates/map-cmdline.slog', force_unix=True)), ('nlog = log --pager=yes --style=%s' % _p('lib/hg/mercurial-cli-templates/map-cmdline.nlog', force_unix=True)), ('sglog = glog --pager=yes --style=%s' % _p('lib/hg/mercurial-cli-templates/map-cmdline.sglog', force_unix=True)), ('nglog = glog --pager=yes --style=%s' % _p('lib/hg/mercurial-cli-templates/map-cmdline.nlog', force_unix=True)), ('blog = glog --page=yes --style=%s' % _p('hgrc/logstyles')), ('wip = glog --pager=yes --style=%s --rev wip' % _p('hgrc/wip.style', force_unix=True)) ]) if is_nix: print("Building fast-hg-prompt...") compile_ok = True try: subprocess.check_call(['make'], cwd=_p('lib/hg/fast-hg-prompt')) except subprocess.CalledProcessError: compile_ok = False for n in ['bookmark', 'remote', 'status']: link_path = os.path.expanduser('~/.local/bin/fast-hg-%s' % n) if compile_ok: mklink('lib/hg/fast-hg-prompt/fast-hg-%s' % n, link_path, mode=(stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)) elif os.path.islink(link_path): os.unlink(link_path) elif os.path.exists(link_path): os.remove(link_path) def install_git(): writelines('~/.gitconfig', [ '[include]', ' path = %s' % _p('git/gitconfig', force_unix=True), '[core]', ' excludesfile = %s' % _p('git/gitignore', force_unix=True) ]) if is_windows: subprocess.check_call( ['setx', 'GIT_SSH', '%USERPROFILE%\\Dropbox\\Utilities\\plink.exe'], shell=True) def install_universal_ctags(): # On Windows, u-ctags has a bug where it outputs double-backslashes. if is_windows: ensure_dir('~/ctags.d') writelines('~/ctags.d/global.ctags', [ '--output-format=e-ctags' ]) @only_on_nix def install_tmux(): mklink('tmux/tmux.conf', '~/.tmux.conf') @only_on_nix def install_weechat(): mklink('weechat', '~/.weechat') @only_on_nix def install_mutt(): if which('gpg2'): gpgbin = 'gpg2' else: if not which('gpg'): print("WARNING: no GPG tools seem to be installed!") gpgbin = 'gpg' writelines('~/.muttrc', [ 'source "%s -dq %s |"' % (gpgbin, _p('mutt/variables.gpg')), 'source "%s"' % _p('mutt/muttrc'), 'source "%s"' % _p('lib/mutt/mutt-colors-solarized/' 'mutt-colors-solarized-dark-256.muttrc') ]) def install_tridactyl(): cfgname = '~/_tridactylrc' if is_windows else '~/.tridactylrc' writelines(cfgname, [ 'source %s' % _p('tridactyl/tridactylrc') ]) def install_qutebrowser(): if is_mac: config_dir = '~/.qutebrowser' elif is_windows: config_dir = '%s/qutebrowser/' % os.getenv('APPDATA') else: config_dir = '~/.config/qutebrowser' mklink('qutebrowser', config_dir) def _on_error_try_make_readable(func, path, exc_info): if not os.access(path, os.W_OK): os.chmod(path, stat.S_IWUSR) func(path) else: raise def _is_non_empty_dir_with(path, contains=None): if not os.path.isdir(path): return False if isinstance(contains, str): contains = [contains] for cnt in contains: if not os.path.exists(os.path.join(path, cnt)): return False return True def clone_git(url, path, force=False): if _is_non_empty_dir_with(path, '.git'): if not force: print("git pull origin master %s" % path) subprocess.check_call(['git', 'pull', 'origin', 'master'], cwd=path) return else: print("Deleting existing: %s" % path) shutil.rmtree(path, onerror=_on_error_try_make_readable) print("git clone %s %s" % (url, path)) ensure_dir(os.path.dirname(path)) subprocess.check_call(['git', 'clone', url, path]) def clone_hg(url, path, force=False): if _is_non_empty_dir_with(path, '.hg'): if not force: print("hg pull -u %s" % path) subprocess.check_call(['hg', 'pull', '-u'], cwd=path) return else: print("Deleting existing: %s" % path) shutil.rmtree(path, onerror=_on_error_try_make_readable) print("hg clone %s %s" % (url, path)) ensure_dir(os.path.dirname(path)) env = dict(os.environ) env.update({'HGPLAIN': '1'}) subprocess.check_call(['hg', 'clone', url, path], env=env) @needs_config @supports_forcing @run_priority(100) def install_subrepos(cfg, force=False): if not cfg.has_section('subrepos'): return for path, url in cfg.items('subrepos'): full_path = _p(path) if url.startswith('[git]'): clone_git(url[len('[git]'):], full_path, force=force) else: clone_hg(url, full_path, force=force) print() @only_on_mac @run_priority(210) def install_xcode(): if shutil.which('xcodebuild') is None: print("Installing XCode") subprocess.check_call(['xcode-select', '--install']) subprocess.check_call(['sudo', 'xcodebuild', '-license', 'accept']) @only_on_mac @run_priority(209) def install_homebrew(): if shutil.which('brew') is None: print("Installing Homebrew and Homebrew Cask") subprocess.check_call([ '/usr/bin/ruby', '-e', "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"]) # NOQA subprocess.check_call(['brew', 'tap', 'caskroom/fonts']) @only_on_mac @needs_config @supports_forcing @run_priority(208) def install_mactools(cfg, force=False): if not cfg.has_section('mactools'): return for name, _ in cfg.items('mactools'): args = ['brew', 'install', name] if force: args.append('--force') subprocess.check_call(args) @only_on_win @run_priority(209) def install_scoop(): if shutil.which('scoop') is None: print("Installing Scoop") subprocess.check_call( '@"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" ' '-NoProfile -InputFormat None -ExecutionPolicy Bypass ' '-Command "iex (new-object net.webclient).downloadstring(\'https://get.scoop.sh\')"') subprocess.check_call( ['scoop.cmd', 'bucket', 'add', 'extras']) else: print("Scoop is already installed") @only_on_win @needs_config @supports_forcing @run_priority(208) def install_wintools(cfg, force=False): if not cfg.has_section('wintools'): return for name, arch in cfg.items('wintools'): args = ['scoop.cmd', 'install', name] if arch: args += ['-a', arch] subprocess.check_call(args) # Main stuff! class FatalInstallerError(Exception): pass def main(): print("dotfiles installer") print("python %s" % sys.version) print("on %s" % sys.platform) print('') cfg = configparser.ConfigParser() cfg.read([ _p('install.cfg'), _p('local', 'local_install.cfg')]) # Get all the methods in this module that are named `install_xxx`. mod_names = ['all'] this_mod = sys.modules[__name__] for an in dir(this_mod): if not an.startswith('install_'): continue name = an[len('install_'):] mod_names.append(name) # See if we have any local install script. local_mod = None local_install_py = _p('local', 'local_install.py') if os.path.isfile(local_install_py): import importlib.util spec = importlib.util.spec_from_file_location('local_install', local_install_py) local_mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(local_mod) sys.modules['local_install'] = local_mod # Create the parser, where you can specify one or more install target. parser = argparse.ArgumentParser() parser.add_argument( 'module', nargs='*', help="Which module(s) to install. Defaults to all modules.") parser.add_argument( '-l', '--list', action='store_true', help="List available modules to install.") parser.add_argument( '-f', '--force', action='store_true', help="Force installation by overwriting things.") args = parser.parse_args() # Print list and exit if needed. if args.list: print("Available modules to install:") for nm in mod_names: print(nm) print() print("Specify 'all' to install all modules.") return # Get the list of methods to run. funcs = [] selected_mods = set(args.module) if 'all' in selected_mods: selected_mods = set(mod_names) selected_mods.remove('all') selected_mods.difference_update([neg[3:] for neg in args.module if neg.startswith('no-')]) for mn in selected_mods: func = getattr(this_mod, 'install_%s' % mn) funcs.append((mn, func)) # See if there's a local method too for this. if local_mod is not None: local_func = getattr(local_mod, 'install_%s' % mn, None) if local_func is not None: lmn = '%s (local)' % mn funcs.append((lmn, local_func)) failed_installs = [] funcs = sorted(funcs, key=_get_install_func_priority, reverse=True) for name, func in funcs: print("Installing %s" % name) f_args = [] f_kwargs = {} if getattr(func, '__dotfiles_needs_config__', False): f_args.append(cfg) if getattr(func, '__dotfiles_supports_forcing__', False): f_kwargs['force'] = args.force try: func(*f_args, **f_kwargs) except Exception as ex: failed_installs.append((name, sys.exc_info())) print("ERROR: %s" % ex) traceback.print_exc() if isinstance(ex, FatalInstallerError): print("Aborting all remaining installs because '%s' failed!" % name) break else: print("Skipping install of '%s'." % name) print() if failed_installs: print() print("----------------------------------") print("ERROR: There were failed installs!") for name, ex_info in failed_installs: print("ERROR: failed to install '%s'." % name) print("ERROR: %s" % ex_info[1]) traceback.print_exception(*ex_info) def _get_install_func_priority(func_info): func = func_info[1] return getattr(func, '__dotfiles_priority__', 0) if __name__ == '__main__': main()