#!/usr/bin/env python3 # FIXME: Add tests. # FIXME: Refine implementation of subcommands. import optparse import os import os.path import re import shutil import sys class VimVersionManager: # Directory structure: # ~/.vvm The root directory for Vim Version Manager. # bin For command-line utilities. # etc For configuration files. # repos Place to store repository of each fork. # $fork # ... # src Place to store source code of each version. # $fork--$tag Naming is the same as ~/.vvm/vims. # ... # vims Place to store deployed versions. # current Symbolic link for an install directory. # $fork--$tag This is the "version" format. # vimorg--v7-3-254 Example: Install directory for the original one. # macvim--v7.3-53 Example: Install directory for MacVim. # ... vvm_repos_uri = 'https://github.com/kana/vim-version-manager.git' # Driver:: def main(self, args): p = VvmOptionParser(version='%prog 2.0.0') (options, left_args) = p.parse_args(args[1:]) if len(left_args) == 0: left_args = ['help'] subcmd_name = left_args[0] try: subcmd_func = getattr(self, 'cmd_%s' % subcmd_name) except AttributeError: die('Subcommand "%s" is not available.' % subcmd_name) subcmd_func(left_args) return # Subcommands:: def cmd_fetch(self, args): '''Fetch source code of a specific version of Vim.''' p = VvmOptionParser() p.usage = '%prog fetch ' (options, left_args) = p.parse_args(args[1:]) if len(left_args) == 0: die('Please specify a version of Vim to fetch its source code.') version = left_args[0] return ( self.fetch(version) and self.checkout(version) ) def cmd_help(self, args): # FIXME: Add more useful description. '''Show help message to use Vim Version Manager.''' subcmd_names = [n for n in dir(self) if n.startswith('cmd_')] subcmd_names.sort() longest_subcmd_name = max(len(n) for n in subcmd_names) print('Available commands:') for n in subcmd_names: print(' %s%s%s' % ( n[len('cmd_'):], ' ' * (longest_subcmd_name - len(n) + 2), getattr(self, n).__doc__.split('\n')[0] )) return True def cmd_install(self, args): '''Install a specific version of Vim.''' p = VvmOptionParser() p.usage = '%prog install [...]' (options, left_args) = p.parse_args(args[1:]) if len(left_args) == 0: die('Please specify a version of Vim to install.') version = left_args[0] configure_options = left_args[1:] vims_dir = get_vims_dir(version) if isdir(vims_dir): echo('%s is already installed.' % version) return return ( self.fetch(version) and self.checkout(version) and self.configure(version, configure_options) and self.make_install(version) ) def cmd_list(self, args): '''List currently installed versions of Vim.''' current_version = get_current_version() versions = listdir(get_vims_dir_itself()) versions.sort() for version in versions: if isdir(get_vims_dir(version)) and version != 'current': mark = '*' if version == current_version else ' ' print(mark, version) return True def cmd_rebuild(self, args): '''Rebuild a specific version of Vim, then install it.''' p = VvmOptionParser() p.usage = '%prog rebuild [...]' (options, left_args) = p.parse_args(args[1:]) if len(left_args) == 0: die('Please specify a version of Vim to rebuild.') version = left_args[0] configure_options = left_args[1:] return ( self.make_clean(version) and self.configure(version, configure_options) and self.make_install(version) ) def cmd_reinstall(self, args): '''Reinstall a specific version of Vim.''' p = VvmOptionParser() p.usage = '%prog reinstall [...]' (options, left_args) = p.parse_args(args[1:]) if len(left_args) == 0: die('Please specify a version of Vim to reinstall.') return ( self.cmd_uninstall(('uninstall',) + tuple(left_args)) and self.cmd_install(('install',) + tuple(left_args)) ) def cmd_setup(self, args): '''Set up files and directories for VVM''' repos_dir = get_repos_dir('vvm') return ( ( has('git') or die('Git is required to set up.') ) and ( not (isdir(get_vvm_dir()) and isdir(repos_dir)) or die('Files and directories for VVM are already set up.') ) and ( sh('git clone %s %s' % ( self.vvm_repos_uri, repos_dir )) == 0 and ln_s('%s/bin' % repos_dir, '%s/bin' % get_vvm_dir()) and ln_s('%s/etc' % repos_dir, '%s/etc' % get_vvm_dir()) and echo('\n'.join(( '', 'VVM is successfully installed. For daily use,', 'please add the following line into your ~/.bash_login etc:', 'test -f ~/.vvm/etc/login && source ~/.vvm/etc/login', ))) ) ) def cmd_uninstall(self, args): '''Uninstall a specific version of Vim.''' p = VvmOptionParser() p.usage = '%prog uninstall ' (options, left_args) = p.parse_args(args[1:]) if len(left_args) == 0: die('Please specify a version of Vim to uninstall.') version = left_args[0] current_path = get_vims_dir('current') src_dir = get_src_dir(version) vims_dir = get_vims_dir(version) if isdir(current_path): target_path = readlink(current_path) if normalize_path(target_path) == normalize_path(vims_dir): die('%s can not be uninstalled; it is currently used.' % version) if isdir(src_dir): rm_r(src_dir) else: print('Something wrong; %s source does not exist.' % version) if isdir(vims_dir): rm_r(vims_dir) else: print('Something wrong; %s binary does not exist.' % version) return True def cmd_update_itself(self, args): '''Update VVM itself''' repos_dir = get_repos_dir('vvm') return ( ( has('git') or die('Git is required to update VVM.') ) and ( isdir(get_vvm_dir()) or die('VVM is not installed. Please set up at the first.') ) and ( sh('cd %s && git pull' % repos_dir) == 0 and echo('\n'.join(( '', 'VVM is successfully updated.', ))) ) ) def cmd_use(self, args): '''Use a specific version of Vim as the default one.''' p = VvmOptionParser() p.usage = '%prog use [--install [...]]' (options, left_args) = p.parse_args(args[1:]) if len(left_args) == 0: die('Please specify a version of Vim to use.') version = left_args[0] if version == 'system': current_path = get_vims_dir('current') if isdir(current_path): rm(current_path) else: vims_dir = get_vims_dir(version) if not isdir(vims_dir): if 2 <= len(left_args) and left_args[1] == '--install': self.cmd_install(('install', version) + tuple(left_args[2:])) or die() else: die('%s is not installed.' % version) current_path = get_vims_dir('current') if isdir(current_path): rm(current_path) ln_s(vims_dir, current_path) return True # Utilities:: def checkout(self, version): vi = VersionInfo.parse(version) try: checkout_func = getattr(self, 'checkout_%s' % vi.vcs) except AttributeError: die('Fork "%s" is not known.' % vi.vcs) return checkout_func(version, vi) def checkout_git(self, version, vi): # FIXME: Refine - Most steps are similar to checkout_mercurial. repos_dir = get_repos_dir(vi.fork) src_dir = get_src_dir(version) return ( (has('git') or die('Git is required to checkout %s.' % vi.fork)) and (isdir(get_src_dir_itself()) or mkdir(get_src_dir_itself())) and ( isdir(src_dir) or sh('cd %s && git archive --prefix=%s/ %s | (cd %s && tar xf -)' % (repos_dir, version, vi.tag, get_src_dir_itself())) == 0 or die() ) ) def checkout_mercurial(self, version, vi): repos_dir = get_repos_dir(vi.fork) src_dir = get_src_dir(version) return ( (has('hg') or die('Mercurial is required to checkout %s.' % vi.fork)) and (isdir(get_src_dir_itself()) or mkdir(get_src_dir_itself())) and ( isdir(src_dir) or sh('cd %s && hg archive -t tar -r %s -p %s - | (cd %s && tar xf -)' % (repos_dir, vi.tag, version, get_src_dir_itself())) == 0 or die() ) ) def configure(self, version, custom_configure_options): default_configure_options = ( '"--prefix=%s"' % os.path.expanduser(get_vims_dir(version)), ) return sh('cd %s && ./configure %s' % ( get_src_dir(version), ' '.join( default_configure_options + tuple("'%s'" % o for o in custom_configure_options) ) )) == 0 def fetch(self, version): vi = VersionInfo.parse(version) try: fetch_func = getattr(self, 'fetch_%s' % vi.vcs) except AttributeError: die('Fork "%s" is not known.' % vi.vcs) return fetch_func(version, vi) def fetch_git(self, version, vi): # FIXME: Refine - Most steps are similar to fetch_mercurial. repos_uri = vi.uri repos_dir = get_repos_dir(vi.fork) src_dir = get_src_dir(version) return ( (has('git') or die('Git is required to install %s.' % vi.fork)) and (isdir(get_repos_dir_itself()) or mkdir(get_repos_dir_itself())) and ( isdir(repos_dir) or sh('git clone --bare %s %s' % (repos_uri, repos_dir)) == 0 or die() ) and (sh('cd %s && git fetch --tags' % repos_dir) == 0 or die()) ) def fetch_mercurial(self, version, vi): repos_uri = vi.uri repos_dir = get_repos_dir(vi.fork) src_dir = get_src_dir(version) return ( (has('hg') or die('Mercurial is required to install %s.' % vi.fork)) and (isdir(get_repos_dir_itself()) or mkdir(get_repos_dir_itself())) and ( isdir(repos_dir) or sh('hg clone %s %s' % (repos_uri, repos_dir)) == 0 or die() ) and (sh('cd %s && hg pull' % repos_dir) == 0 or die()) ) def make_clean(self, version): return make(version, ('clean',)) def make_install(self, version): return make(version, ('all', 'install',)) def get_current_version(): try: path = os.path.basename(readlink(get_vims_dir('current'))) return path except: return None def get_etc_file(filename): return '%s/%s' % (get_etc_dir_itself(), filename) def get_etc_dir_itself(): return '~/.vvm/etc' def get_repos_dir(fork): return '%s/%s' % (get_repos_dir_itself(), fork) def get_repos_dir_itself(): return '~/.vvm/repos' def get_src_dir(version): return '%s/%s' % (get_src_dir_itself(), version) def get_src_dir_itself(): return '~/.vvm/src' def get_vims_dir(version): return '%s/%s' % (get_vims_dir_itself(), version) def get_vims_dir_itself(): return '~/.vvm/vims' def get_vvm_dir(): return '~/.vvm' class VvmOptionParser(optparse.OptionParser): def __init__(self, *args, **kw): optparse.OptionParser.__init__(self, *args, **kw) self.disable_interspersed_args() return def parse_args(self, args): # optparse.OptionParser.parse_args doesn't accespt tuple. return optparse.OptionParser.parse_args(self, list(args)) class VersionInfo(object): __slots__ = ( 'fork', 'tag', 'uri', 'vcs', ) def __init__(self): return @classmethod def parse(cls, version_string): (fork_name, tag_name) = version_string.split('--') try: known_fork = cls.get_dict()[fork_name] except KeyError: die('\n'.join(( 'Fork %s is not known to VVM.', 'Edit %s to direct VVM how to fetch the fork.', )) % (fork_name, get_etc_file('repos-map'))) vi = VersionInfo() vi.fork = fork_name vi.tag = tag_name vi.uri = known_fork.uri vi.vcs = known_fork.vcs return vi @classmethod def read_config_files(cls): vi_dict = {} for filename in ('repos-map.default', 'repos-map'): path = os.path.expanduser(get_etc_file(filename)) if not isfile(path): continue for line in open(path): if re.match(r'^\s*$', line) or re.match(r'^\s*#', line): continue else: vi = cls() (vi.fork, vi.vcs, vi.uri) = line.split() vi_dict[vi.fork] = vi return vi_dict @classmethod def get_dict(cls): if cls.__vi_dict is None: cls.__vi_dict = cls.read_config_files() return cls.__vi_dict __vi_dict = None def die(message=''): return exit(message) def echo(message): print(message) return True def has(progname): return sh('which %s >/dev/null 2>/dev/null' % progname) == 0 def isdir(path): return os.path.isdir(os.path.expanduser(path)) def isfile(path): return os.path.isfile(os.path.expanduser(path)) def listdir(path): return os.listdir(os.path.expanduser(path)) def ln_s(target, link_name): os.symlink(os.path.expanduser(target), os.path.expanduser(link_name)) return True def make(version, args): return sh('cd %s && make %s' % ( get_src_dir(version), ' '.join(args) )) == 0 def mkdir(path): os.makedirs(os.path.expanduser(path), 0o755) return True def normalize_path(path): return os.path.abspath(os.path.expanduser(path)) def readlink(path): return os.readlink(os.path.expanduser(path)) def rm(path): os.remove(os.path.expanduser(path)) return True def rm_r(path): shutil.rmtree(os.path.expanduser(path)) return True def sh(cmdline): return os.system(cmdline) if __name__ == '__main__': VimVersionManager().main(sys.argv) # __END__