diff install.py @ 416:222b477ad678

Install script improvements. - Support a local script that adds more installs. - Add `--force` flag to force certain things.
author Ludovic Chabant <ludovic@chabant.com>
date Fri, 19 Jan 2018 09:19:34 -0800
parents c6da0c9f40ae
children 6dbef23ca6bd
line wrap: on
line diff
--- a/install.py	Fri Jan 19 09:18:30 2018 -0800
+++ b/install.py	Fri Jan 19 09:19:34 2018 -0800
@@ -2,6 +2,7 @@
 import os.path
 import sys
 import stat
+import shutil
 import argparse
 import functools
 import subprocess
@@ -17,8 +18,13 @@
     is_windows = True
 
 
-def _p(*paths):
-    return os.path.join(dotfiles_dir, *paths).replace('/', os.sep)
+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):
@@ -72,6 +78,11 @@
     return decorator
 
 
+def supports_forcing(f):
+    f.__dotfiles_supports_forcing__ = True
+    return f
+
+
 def needs_config(f):
     f.__dotfiles_needs_config__ = True
     return f
@@ -147,7 +158,7 @@
 def install_git():
     writelines('~/.gitconfig', [
         '[include]',
-        'path = %s' % _p('git/gitconfig')
+        'path = %s' % _p('git/gitconfig', force_unix=True)
     ])
     if is_windows:
         subprocess.check_call(
@@ -174,19 +185,39 @@
         '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):
+def clone_git(url, path, force=False):
     if os.path.isdir(path):
-        print("Skipping git clone of %s -- directory exists" % path)
+        if not force:
+            print("Skipping git clone of %s -- directory exists" % 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):
+def clone_hg(url, path, force=False):
     if os.path.isdir(path):
-        print("Skipping hg clone of %s -- directory exists" % path)
+        if not force:
+            print("Skipping hg clone of %s -- directory exists" % 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)
@@ -195,17 +226,18 @@
 
 
 @needs_config
+@supports_forcing
 @run_priority(100)
-def install_subrepos(cfg):
+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)
+            clone_git(url[len('[git]'):], full_path, force=force)
         else:
-            clone_hg(url, full_path)
+            clone_hg(url, full_path, force=force)
 
 
 def main():
@@ -217,6 +249,7 @@
     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):
@@ -225,14 +258,31 @@
 
         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:
@@ -241,14 +291,29 @@
     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)
-        if hasattr(func, '__dotfiles_needs_config__'):
-            func(cfg)
-        else:
-            func()
+
+        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):