view install.py @ 427:9a046e8fe5dd

Better way to figure out the OS in Fish.
author Ludovic Chabant <ludovic@chabant.com>
date Thu, 29 Mar 2018 12:56:11 -0700
parents 350f7a55ff33
children e0bb52007402
line wrap: on
line source

import os
import os.path
import sys
import stat
import shutil
import argparse
import functools
import subprocess
import configparser


dotfiles_dir = os.path.abspath(os.path.dirname(__file__))

is_nix = True
is_windows = False
if sys.platform == "win32":
    is_nix = False
    is_windows = True


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))
    os.symlink(orig_full_path, link_full_path)
    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 only_on_nix(f):
    @functools.wraps(f)
    def decorator(*args, **kwargs):
        if is_nix:
            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


@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():
    vimrc_path = '~/.vimrc'
    if is_windows:
        vimrc_path = '~/_vimrc'
    writelines(vimrc_path, [
        'set runtimepath+=%s' % nixslash(_p('vim')),
        'source %s' % nixslash(_p('vim', 'vimrc'))
    ])


@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/mutable-history/hgext3rd/evolve'),
        'terse-status = %s' % _p('lib/hg/terse-status/terse-status.py')
    ])
    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)
    ])
    if is_windows:
        subprocess.check_call(
            ['setx', 'GIT_SSH',
             '%USERPROFILE%\\Dropbox\\Utilities\\plink.exe'],
            shell=True)


@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():
    writelines('~/.muttrc', [
        'source "gpg2 -dq %s |"' % _p('mutt/variables.gpg'),
        'source "%s"' % _p('mutt/muttrc'),
        'source "%s"' % _p('lib/mutt/mutt-colors-solarized/'
                           'mutt-colors-solarized-dark-256.muttrc')
    ])


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 clone_git(url, path, force=False):
    if os.path.isdir(path):
        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 os.path.isdir(path):
        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)


def main():
    print("dotfiles installer")
    print("python %s" % sys.version)
    print("on %s" % sys.platform)
    print('')

    cfg = configparser.ConfigParser()
    cfg.read(_p('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 = os.path.join(dotfiles_dir, '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='*',
        choices=mod_names,
        help="Which module(s) to install. Defaults to all modules.")
    parser.add_argument(
        '-f', '--force', action='store_true',
        help="Force installation by overwriting things.")
    args = parser.parse_args()

    # 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')
    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))

    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:
            print("ERROR: %s" % ex)
            print("Aborting install.")


def _get_install_func_priority(func_info):
    func = func_info[1]
    return getattr(func, '__dotfiles_priority__', 0)


if __name__ == '__main__':
    main()