summaryrefslogtreecommitdiff
path: root/dotbot
diff options
context:
space:
mode:
authorAnish Athalye <me@anishathalye.com>2018-06-11 21:14:10 -0400
committerAnish Athalye <me@anishathalye.com>2018-06-11 21:14:10 -0400
commit9d967ad7e8f22c6426d559dd211f1cba766a687e (patch)
treee07cfb532fc620255d35c3ed6a7b837194c6c783 /dotbot
parentf7a8bf10ba1d4073f6501ea1518d0e94ded4ac0e (diff)
Include built-in plugins in PyPI distribution
Diffstat (limited to 'dotbot')
-rw-r--r--dotbot/cli.py2
-rw-r--r--dotbot/plugins/__init__.py3
-rw-r--r--dotbot/plugins/clean.py57
-rw-r--r--dotbot/plugins/link.py219
-rw-r--r--dotbot/plugins/shell.py61
5 files changed, 341 insertions, 1 deletions
diff --git a/dotbot/cli.py b/dotbot/cli.py
index 0674cbe..aec6097 100644
--- a/dotbot/cli.py
+++ b/dotbot/cli.py
@@ -52,7 +52,7 @@ def main():
log.set_level(Level.DEBUG)
plugin_directories = list(options.plugin_dirs)
if not options.disable_built_in_plugins:
- plugin_directories.append(os.path.join(os.path.dirname(__file__), '..', 'plugins'))
+ from .plugins import Clean, Link, Shell
plugin_paths = []
for directory in plugin_directories:
for plugin_path in glob.glob(os.path.join(directory, '*.py')):
diff --git a/dotbot/plugins/__init__.py b/dotbot/plugins/__init__.py
new file mode 100644
index 0000000..93bd981
--- /dev/null
+++ b/dotbot/plugins/__init__.py
@@ -0,0 +1,3 @@
+from .clean import Clean
+from .link import Link
+from .shell import Shell
diff --git a/dotbot/plugins/clean.py b/dotbot/plugins/clean.py
new file mode 100644
index 0000000..7e6cba1
--- /dev/null
+++ b/dotbot/plugins/clean.py
@@ -0,0 +1,57 @@
+import os, dotbot
+
+class Clean(dotbot.Plugin):
+ '''
+ Cleans broken symbolic links.
+ '''
+
+ _directive = 'clean'
+
+ def can_handle(self, directive):
+ return directive == self._directive
+
+ def handle(self, directive, data):
+ if directive != self._directive:
+ raise ValueError('Clean cannot handle directive %s' % directive)
+ return self._process_clean(data)
+
+ def _process_clean(self, targets):
+ success = True
+ defaults = self._context.defaults().get(self._directive, {})
+ force = defaults.get('force', False)
+ for target in targets:
+ if isinstance(targets, dict):
+ force = targets[target].get('force', force)
+ success &= self._clean(target, force)
+ if success:
+ self._log.info('All targets have been cleaned')
+ else:
+ self._log.error('Some targets were not successfully cleaned')
+ return success
+
+ def _clean(self, target, force):
+ '''
+ Cleans all the broken symbolic links in target if they point to
+ a subdirectory of the base directory or if forced to clean.
+ '''
+ if not os.path.isdir(os.path.expanduser(target)):
+ self._log.debug('Ignoring nonexistent directory %s' % target)
+ return True
+ for item in os.listdir(os.path.expanduser(target)):
+ path = os.path.join(os.path.expanduser(target), item)
+ if not os.path.exists(path) and os.path.islink(path):
+ points_at = os.path.join(os.path.dirname(path), os.readlink(path))
+ if self._in_directory(path, self._context.base_directory()) or force:
+ self._log.lowinfo('Removing invalid link %s -> %s' % (path, points_at))
+ os.remove(path)
+ else:
+ self._log.lowinfo('Link %s -> %s not removed.' % (path, points_at))
+ return True
+
+ def _in_directory(self, path, directory):
+ '''
+ Returns true if the path is in the directory.
+ '''
+ directory = os.path.join(os.path.realpath(directory), '')
+ path = os.path.realpath(path)
+ return os.path.commonprefix([path, directory]) == directory
diff --git a/dotbot/plugins/link.py b/dotbot/plugins/link.py
new file mode 100644
index 0000000..5274e3b
--- /dev/null
+++ b/dotbot/plugins/link.py
@@ -0,0 +1,219 @@
+import os
+import glob
+import shutil
+import dotbot
+
+
+class Link(dotbot.Plugin):
+ '''
+ Symbolically links dotfiles.
+ '''
+
+ _directive = 'link'
+
+ def can_handle(self, directive):
+ return directive == self._directive
+
+ def handle(self, directive, data):
+ if directive != self._directive:
+ raise ValueError('Link cannot handle directive %s' % directive)
+ return self._process_links(data)
+
+ def _process_links(self, links):
+ success = True
+ defaults = self._context.defaults().get('link', {})
+ for destination, source in links.items():
+ destination = os.path.expandvars(destination)
+ relative = defaults.get('relative', False)
+ force = defaults.get('force', False)
+ relink = defaults.get('relink', False)
+ create = defaults.get('create', False)
+ use_glob = defaults.get('glob', False)
+ if isinstance(source, dict):
+ # extended config
+ relative = source.get('relative', relative)
+ force = source.get('force', force)
+ relink = source.get('relink', relink)
+ create = source.get('create', create)
+ use_glob = source.get('glob', use_glob)
+ path = self._default_source(destination, source.get('path'))
+ else:
+ path = self._default_source(destination, source)
+ path = os.path.expandvars(os.path.expanduser(path))
+ if use_glob:
+ self._log.debug("Globbing with path: " + str(path))
+ glob_results = glob.glob(path)
+ if len(glob_results) is 0:
+ self._log.warning("Globbing couldn't find anything matching " + str(path))
+ success = False
+ continue
+ glob_star_loc = path.find('*')
+ if glob_star_loc is -1 and destination[-1] is '/':
+ self._log.error("Ambiguous action requested.")
+ self._log.error("No wildcard in glob, directory use undefined: " +
+ destination + " -> " + str(glob_results))
+ self._log.warning("Did you want to link the directory or into it?")
+ success = False
+ continue
+ elif glob_star_loc is -1 and len(glob_results) is 1:
+ # perform a normal link operation
+ if create:
+ success &= self._create(destination)
+ if force or relink:
+ success &= self._delete(path, destination, relative, force)
+ success &= self._link(path, destination, relative)
+ else:
+ self._log.lowinfo("Globs from '" + path + "': " + str(glob_results))
+ glob_base = path[:glob_star_loc]
+ for glob_full_item in glob_results:
+ glob_item = glob_full_item[len(glob_base):]
+ glob_link_destination = os.path.join(destination, glob_item)
+ if create:
+ success &= self._create(glob_link_destination)
+ if force or relink:
+ success &= self._delete(glob_full_item, glob_link_destination, relative, force)
+ success &= self._link(glob_full_item, glob_link_destination, relative)
+ else:
+ if create:
+ success &= self._create(destination)
+ if not self._exists(os.path.join(self._context.base_directory(), path)):
+ success = False
+ self._log.warning('Nonexistent target %s -> %s' %
+ (destination, path))
+ continue
+ if force or relink:
+ success &= self._delete(path, destination, relative, force)
+ success &= self._link(path, destination, relative)
+ if success:
+ self._log.info('All links have been set up')
+ else:
+ self._log.error('Some links were not successfully set up')
+ return success
+
+ def _default_source(self, destination, source):
+ if source is None:
+ basename = os.path.basename(destination)
+ if basename.startswith('.'):
+ return basename[1:]
+ else:
+ return basename
+ else:
+ return source
+
+ def _is_link(self, path):
+ '''
+ Returns true if the path is a symbolic link.
+ '''
+ return os.path.islink(os.path.expanduser(path))
+
+ def _link_destination(self, path):
+ '''
+ Returns the destination of the symbolic link.
+ '''
+ path = os.path.expanduser(path)
+ return os.readlink(path)
+
+ def _exists(self, path):
+ '''
+ Returns true if the path exists.
+ '''
+ path = os.path.expanduser(path)
+ return os.path.exists(path)
+
+ def _create(self, path):
+ success = True
+ parent = os.path.abspath(os.path.join(os.path.expanduser(path), os.pardir))
+ if not self._exists(parent):
+ self._log.debug("Try to create parent: " + str(parent))
+ try:
+ os.makedirs(parent)
+ except OSError:
+ self._log.warning('Failed to create directory %s' % parent)
+ success = False
+ else:
+ self._log.lowinfo('Creating directory %s' % parent)
+ return success
+
+ def _delete(self, source, path, relative, force):
+ success = True
+ source = os.path.join(self._context.base_directory(), source)
+ fullpath = os.path.expanduser(path)
+ if relative:
+ source = self._relative_path(source, fullpath)
+ if ((self._is_link(path) and self._link_destination(path) != source) or
+ (self._exists(path) and not self._is_link(path))):
+ removed = False
+ try:
+ if os.path.islink(fullpath):
+ os.unlink(fullpath)
+ removed = True
+ elif force:
+ if os.path.isdir(fullpath):
+ shutil.rmtree(fullpath)
+ removed = True
+ else:
+ os.remove(fullpath)
+ removed = True
+ except OSError:
+ self._log.warning('Failed to remove %s' % path)
+ success = False
+ else:
+ if removed:
+ self._log.lowinfo('Removing %s' % path)
+ return success
+
+ def _relative_path(self, source, destination):
+ '''
+ Returns the relative path to get to the source file from the
+ destination file.
+ '''
+ destination_dir = os.path.dirname(destination)
+ return os.path.relpath(source, destination_dir)
+
+ def _link(self, source, link_name, relative):
+ '''
+ Links link_name to source.
+
+ Returns true if successfully linked files.
+ '''
+ success = False
+ destination = os.path.expanduser(link_name)
+ absolute_source = os.path.join(self._context.base_directory(), source)
+ if relative:
+ source = self._relative_path(absolute_source, destination)
+ else:
+ source = absolute_source
+ if (not self._exists(link_name) and self._is_link(link_name) and
+ self._link_destination(link_name) != source):
+ self._log.warning('Invalid link %s -> %s' %
+ (link_name, self._link_destination(link_name)))
+ # we need to use absolute_source below because our cwd is the dotfiles
+ # directory, and if source is relative, it will be relative to the
+ # destination directory
+ elif not self._exists(link_name) and self._exists(absolute_source):
+ try:
+ os.symlink(source, destination)
+ except OSError:
+ self._log.warning('Linking failed %s -> %s' % (link_name, source))
+ else:
+ self._log.lowinfo('Creating link %s -> %s' % (link_name, source))
+ success = True
+ elif self._exists(link_name) and not self._is_link(link_name):
+ self._log.warning(
+ '%s already exists but is a regular file or directory' %
+ link_name)
+ elif self._is_link(link_name) and self._link_destination(link_name) != source:
+ self._log.warning('Incorrect link %s -> %s' %
+ (link_name, self._link_destination(link_name)))
+ # again, we use absolute_source to check for existence
+ elif not self._exists(absolute_source):
+ if self._is_link(link_name):
+ self._log.warning('Nonexistent target %s -> %s' %
+ (link_name, source))
+ else:
+ self._log.warning('Nonexistent target for %s : %s' %
+ (link_name, source))
+ else:
+ self._log.lowinfo('Link exists %s -> %s' % (link_name, source))
+ success = True
+ return success
diff --git a/dotbot/plugins/shell.py b/dotbot/plugins/shell.py
new file mode 100644
index 0000000..b6f5184
--- /dev/null
+++ b/dotbot/plugins/shell.py
@@ -0,0 +1,61 @@
+import os, subprocess, dotbot
+
+class Shell(dotbot.Plugin):
+ '''
+ Run arbitrary shell commands.
+ '''
+
+ _directive = 'shell'
+
+ def can_handle(self, directive):
+ return directive == self._directive
+
+ def handle(self, directive, data):
+ if directive != self._directive:
+ raise ValueError('Shell cannot handle directive %s' %
+ directive)
+ return self._process_commands(data)
+
+ def _process_commands(self, data):
+ success = True
+ defaults = self._context.defaults().get('shell', {})
+ with open(os.devnull, 'w') as devnull:
+ for item in data:
+ stdin = stdout = stderr = devnull
+ if defaults.get('stdin', False) == True:
+ stdin = None
+ if defaults.get('stdout', False) == True:
+ stdout = None
+ if defaults.get('stderr', False) == True:
+ stderr = None
+ if isinstance(item, dict):
+ cmd = item['command']
+ msg = item.get('description', None)
+ if 'stdin' in item:
+ stdin = None if item['stdin'] == True else devnull
+ if 'stdout' in item:
+ stdout = None if item['stdout'] == True else devnull
+ if 'stderr' in item:
+ stderr = None if item['stderr'] == True else devnull
+ elif isinstance(item, list):
+ cmd = item[0]
+ msg = item[1] if len(item) > 1 else None
+ else:
+ cmd = item
+ msg = None
+ if msg is None:
+ self._log.lowinfo(cmd)
+ else:
+ self._log.lowinfo('%s [%s]' % (msg, cmd))
+ executable = os.environ.get('SHELL')
+ ret = subprocess.call(cmd, shell=True, stdin=stdin, stdout=stdout,
+ stderr=stderr, cwd=self._context.base_directory(),
+ executable=executable)
+ if ret != 0:
+ success = False
+ self._log.warning('Command [%s] failed' % cmd)
+ if success:
+ self._log.info('All commands have been executed')
+ else:
+ self._log.error('Some commands were not successfully executed')
+ return success