diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Makefile-files | 64 | ||||
-rw-r--r-- | src/nbb.in | 47 | ||||
-rw-r--r-- | src/nbblib/__init__.py | 6 | ||||
-rw-r--r-- | src/nbblib/bs.py | 126 | ||||
-rw-r--r-- | src/nbblib/commands.py | 274 | ||||
-rw-r--r-- | src/nbblib/main.py | 229 | ||||
-rw-r--r-- | src/nbblib/package.in | 8 | ||||
-rw-r--r-- | src/nbblib/plugins.py | 54 | ||||
-rw-r--r-- | src/nbblib/progutils.py | 55 | ||||
-rw-r--r-- | src/nbblib/vcs.py | 217 |
10 files changed, 1080 insertions, 0 deletions
diff --git a/src/Makefile-files b/src/Makefile-files new file mode 100644 index 0000000..6707adc --- /dev/null +++ b/src/Makefile-files @@ -0,0 +1,64 @@ +# -*- makefile -*- + +if HAVE_PYTHON + +nodist_nbblib_PYTHON += src/nbblib/package.py +CLEANFILES += src/nbblib/package.py + +nbblib_PYTHON += src/nbblib/__init__.py +nbblib_PYTHON += src/nbblib/bs.py +nbblib_PYTHON += src/nbblib/commands.py +nbblib_PYTHON += src/nbblib/main.py +nbblib_PYTHON += src/nbblib/plugins.py +nbblib_PYTHON += src/nbblib/progutils.py +nbblib_PYTHON += src/nbblib/vcs.py + +# Put all python source files, whether changed or verbatim, +# into builddir, such that we can run tests in builddir. +all-local: all-local-nbblib +all-local-nbblib: + @for f in $(nbblib_PYTHON); do \ + if test -f "$(top_builddir)/$$f" && test "$(top_srcdir)/$$f" -ot "$(top_builddir)/$$f"; then :; else \ + echo "INFO: Updating $$f in $(top_builddir) from $(top_srcdir)"; \ + cp -f "$(top_srcdir)/$$f" "$(top_builddir)/$$f"; \ + fi; \ + done + +bin_SCRIPTS += src/nbb +CLEANFILES += src/nbb + +# We cannot create src/nbb from src/nbb.in in configure.ac/config.status. +# pythondir is defined as ${something}foobar, and that needs expansion. +EXTRA_DIST += src/nbb.in +src/nbb: src/nbb.in $(nodist_nbblib_PYTHON) $(nbblib_PYTHON) Makefile + $(SED) \ + -e 's&[@]pythondir@&$(pythondir)&g' \ + -e 's&[@]PYTHON@&$(PYTHON)&g' \ + -e 's&[@]PACKAGE_VERSION@&$(PACKAGE_VERSION)&g' \ + < $(srcdir)/src/nbb.in > src/nbb.new + @if test "x$$($(GREP) '@[a-zA-Z0-9_]\{1,\}@' src/nbb.new)" = "x"; then :; \ + else \ + echo "FATAL: Unsubstituted markers remain in src/nbb.new."; \ + $(GREP) '@[a-zA-Z0-9_]\{1,\}@' src/nbb.new; \ + exit 1; \ + fi + @if test -f src/nbb && cmp src/nbb.new src/nbb; \ + then rm -f src/nbb.new; \ + else mv -f src/nbb.new src/nbb; echo "INFO: Updating src/nbb"; fi + @chmod +x src/nbb + +endif + +clean-local: clean-local-nbblib +clean-local-nbblib: + rm -f src/nbblib/*.pyc + @top_srcdir="$$(cd "$(top_srcdir)" > /dev/null 2>&1 && pwd)"; \ + top_builddir="$$(cd "$(top_builddir)" > /dev/null 2>&1 && pwd)"; \ + if test "x$${top_srcdir}" = "x$${top_builddir}"; then :; else \ + for f in $(nbblib_PYTHON); do \ + echo rm -f "$(top_builddir)/$$f"; \ + rm -f "$(top_builddir)/$$f"; \ + done; \ + fi + +# End of Makefile-files. diff --git a/src/nbb.in b/src/nbb.in new file mode 100644 index 0000000..ac2c2f0 --- /dev/null +++ b/src/nbb.in @@ -0,0 +1,47 @@ +#!@PYTHON@ +"""\ +nbb - ndim's branch builder +Build, install given branch of source code into a branch specific place +Copyright (C) 2007, 2008 Hans Ulrich Niedermann <hun@n-dimensional.de> +License conditions TBA +""" + +import sys +import os + +PACKAGE_VERSION = "@PACKAGE_VERSION@" + +if __name__ == '__main__': + pythondir = "@pythondir@" + lib_found = False + #print "pythondir", pythondir + #print "sys.path", sys.path + sys.stdout.flush() + orig_path = sys.path + for cond, path in [ + (True, orig_path), + (os.path.exists(pythondir), [pythondir] + orig_path), + ]: + if cond: + sys.path = path + try: + import nbblib + #print "nbblib.PACKAGE_VERSION", nbblib.PACKAGE_VERSION + #print "PACKAGE_VERSION", PACKAGE_VERSION + assert(nbblib.PACKAGE_VERSION == PACKAGE_VERSION) + lib_found = True + break + except AssertionError, e: + sys.path = orig_path + except ImportError, e: + sys.path = orig_path + if not lib_found: + sys.stderr.write("nbb: Fatal: Could not load nbblib.\n") + sys.exit(3) + import nbblib.main + nbblib.main.main(sys.argv) + +# vim: syntax=python +# Local Variables: +# mode: python +# End: diff --git a/src/nbblib/__init__.py b/src/nbblib/__init__.py new file mode 100644 index 0000000..d919ff1 --- /dev/null +++ b/src/nbblib/__init__.py @@ -0,0 +1,6 @@ +from nbblib.bs import * +from nbblib.commands import * +from nbblib.package import * +from nbblib.plugins import * +from nbblib.vcs import * +from nbblib.bs import * diff --git a/src/nbblib/bs.py b/src/nbblib/bs.py new file mode 100644 index 0000000..cb498cb --- /dev/null +++ b/src/nbblib/bs.py @@ -0,0 +1,126 @@ +######################################################################## +# Buildsystem Source Tree plugins +######################################################################## + + +import os +from nbblib.plugins import * +from nbblib.progutils import * + + +class NotABSSourceTree(Exception): + def __init__(self, vcs_tree): + super(NotABSSourceTree, self).__init__() + self.vcs_tree = vcs_tree + def __str__(self): + return ("Source tree build system type for '%s' not detected" + % (self.vcs_tree,)) + + +class AmbigousBSSource(Exception): + def __init__(self, srcdir, matches): + super(AmbigousBSSource, self).__init__() + self.srcdir = srcdir + self.matches = matches + def __str__(self): + fmt = " %-9s %s" + def strmatch(m): + return fmt % (m.name, m.tree_root()) + alist = [fmt % ('VCS Type', 'Source tree root')] + alist.extend(map(strmatch, self.matches)) + return ("More than one source tree VCS type detected for '%s':\n#%s" + % (self.srcdir, '\n '.join(alist))) + + +class BSSourceTree(object): + __metaclass__ = GenericPluginMeta + + def __init__(self, context): + super(BSSourceTree, self).__init__() + self.context = context + + @classmethod + def detect(cls, vcs_tree, context): + """Find BS tree type and return it""" + if len(BSSourceTree.plugins) < 1: + raise "No BS source tree classes registered" + matches = PluginDict() + for key, klass in BSSourceTree.plugins.items(): + try: + t = klass(vcs_tree, context) + if t.tree_root() == vcs_tree.tree_root(): + matches[key] = t + except NotABSSourceTree, e: + pass + if len(matches) > 1: + raise ("More than one source tree BS type detected for '%s': %s" + % (vcs_tree, ", ".join(map(lambda x:str(x), matches)))) + elif len(matches) < 1: + raise NotABSSourceTree(vcs_tree) + return matches[matches.keys()[0]] + + def __str__(self): + return "BS-Source-Tree(%s, %s)" % (self.name, + repr(self.tree_root())) + + # Abstract methods + def tree_root(self): raise NotImplementedError() + def init(self): raise NotImplementedError() + def configure(self): raise NotImplementedError() + def build(self): raise NotImplementedError() + def install(self): raise NotImplementedError() + + +class AutomakeSourceTree(BSSourceTree): + name = 'automake' + def __init__(self, vcs_tree, context): + super(AutomakeSourceTree, self).__init__(context) + srcdir = vcs_tree.tree_root() + self.config = vcs_tree.config + flag = False + for f in [ os.path.join(srcdir, 'configure.ac'), + os.path.join(srcdir, 'configure.in'), + ]: + if os.path.exists(f): + flag = True + break + if not flag: + raise NotABSSourceTree(vcs_tree) + + def tree_root(self): + return self.config.srcdir + + def init(self): + """'autoreconf'""" + prog_run(["autoreconf", "-v", "-i", "-s", self.config.srcdir], + self.context) + + def configure(self): + """'configure --prefix'""" + builddir = self.config.builddir + if not os.path.exists(builddir): os.makedirs(builddir) + if not os.path.exists(os.path.join(builddir, 'configure')): + self.init + os.chdir(builddir) + prog_run(["%s/configure" % self.config.srcdir, + "--prefix=%s" % self.config.installdir + ], self.context) + + def build(self): + """'make'""" + builddir = self.config.builddir + if not os.path.exists(os.path.join(builddir, 'config.status')): + self.configure() + os.chdir(builddir) + prog_run(["make", ], self.context) + + def install(self): + """'make install'""" + builddir = self.config.builddir + if not os.path.exists(os.path.join(builddir, 'config.status')): + self.configure() + os.chdir(builddir) + prog_run(["make", "install", "INSTALL=/usr/bin/install -p"], + self.context) + + diff --git a/src/nbblib/commands.py b/src/nbblib/commands.py new file mode 100644 index 0000000..45e3a61 --- /dev/null +++ b/src/nbblib/commands.py @@ -0,0 +1,274 @@ +import os +import sys + + +from nbblib.package import * +from nbblib.plugins import * +from nbblib.progutils import * +from nbblib.vcs import * +from nbblib.bs import * + + +def adjust_doc(doc): + """Remove common whitespace at beginning of doc string lines""" + if not doc: return doc + i = 0 + for i in range(len(doc)): + if doc[i] not in " \t": + break + prefix = doc[:i] + rest_doc = doc[i:] + almost_doc = rest_doc.replace("\n%s" % prefix, "\n") + i = -1 + while almost_doc[i] == '\n': + i = i - 1 + return almost_doc[:i] + + +class CommandLineError(Exception): + def __init__(self, message, *args): + super(CommandLineError, self).__init__() + if args: + self.msg = message % args + else: + self.msg = message + def __str__(self): + return "Command line error: %s" % self.msg + + +######################################################################## +# Command plugin system +######################################################################## + +class Command(object): + """ + Mount point for plugins which refer to commands that can be performed. + + Plugins implementing this reference should provide the following + interface: + + name attribute + The text to be displayed, describing the version control system + summary attribute + Short (less than 50 chars) command summary line + usage attribute + Usage string (defaults to '') + + validate_args(*args, **kwargs) function + Must raise CommandLineError() if it encounters invalid arguments in cmdargs + run() function + Actually run the function + + FFF(*args, **kwargs) + *args are the arguments from the command line + **kwargs are additional parameters from within the program + """ + __metaclass__ = GenericPluginMeta + + usage = '' + + def __init__(self, *args, **kwargs): + self.validate_args(*args, **kwargs) + self.args = args + self.kwargs = kwargs + self.context = kwargs['context'] + + def run(self): + """Run the command""" + raise NotImplementedError() + + def validate_args(self, *args, **kwargs): + """Validate command line arguments""" + print "Command: ", self.name + print "*args: ", args + print "**kwargs:", kwargs + if len(args) > 0: + raise CommandLineError("'%s' command takes no parameters", + self.name) + + def __str__(self): + return "Command(%s, %s)" % (self.cmd_name, self.cmdargs) + + +class HelpCommand(Command): + """\ + If the optional <command> is given, print the help for <command>. + Else, print a list of commands and general help. + """ + + name = 'help' + summary = 'print help text' + usage = '[<command>]' + + def validate_args(self, *args, **kwargs): + if len(args) == 1 and args[0] not in Command.plugins.keys(): + raise CommandLineError("'%s' is an invalid command name", args[0]) + elif len(args) > 1: + raise CommandLineError("'%s' command only takes one optional parameter", self.name) + + def _print_command_list(self): + print "List of commands:" + keys = Command.plugins.keys() + keys.sort() + for k in keys: + print "\t%-15s\t%s" % (k, Command.plugins[k].summary) + + def _print_command_help(self, cmd): + """print help for command cmd""" + c = Command.plugins[cmd] + print "Purpose:", c.summary + if c.usage: + print "Usage: ", self.context.prog, cmd, c.usage + else: + print "Usage: ", self.context.prog, cmd + if hasattr(c, '__doc__'): + if c.__doc__: + print + print adjust_doc(c.__doc__) + + def run(self): + if len(self.args) == 0: + self._print_command_list() + elif len(self.args) == 1: + self._print_command_help(self.args[0]) + else: + assert(False) + + +class InternalConfigCommand(Command): + name = 'internal-config' + summary = 'print internal program configuration' + def run(self): + print "Source tree types:", ", ".join(VCSourceTree.plugins.keys()) + print "Build system types:", ", ".join(BSSourceTree.plugins.keys()) + print "Commands:", ", ".join(Command.plugins.keys()) + + +class SourceClassCommand(Command): + """Base class for commands acting on source trees""" + def __init__(self, *args, **kwargs): + super(SourceClassCommand, self).__init__(*args, **kwargs) + context = kwargs['context'] + srcdir = os.getcwd() + absdir = os.path.abspath(srcdir) + self.vcs_sourcetree = VCSourceTree.detect(absdir, context) + if context.verbose: + print "vcs_sourcetree", str(self.vcs_sourcetree) + assert(self.vcs_sourcetree) + cfg = self.vcs_sourcetree.config + self.bs_sourcetree = BSSourceTree.detect(self.vcs_sourcetree, context) + if context.verbose: + print "bs_sourcetree", str(self.bs_sourcetree) + print "CONFIG", cfg + print " ", "srcdir", cfg.srcdir + print " ", "builddir", cfg.builddir + print " ", "installdir", cfg.installdir + + +class BuildTestCommand(SourceClassCommand): + name = 'build-test' + summary = 'simple build test' + def run(self): + self.bs_sourcetree.init() + self.bs_sourcetree.configure() + self.bs_sourcetree.build() + self.bs_sourcetree.install() + + +class InitCommand(SourceClassCommand): + name = 'init' + summary = 'initialize buildsystem' + def run(self): + self.bs_sourcetree.init() + +class ConfigureCommand(SourceClassCommand): + name = 'configure' + summary = 'configure buildsystem' + def run(self): + self.bs_sourcetree.configure() + +class BuildCommand(SourceClassCommand): + name = 'build' + summary = 'build from source' + def run(self): + self.bs_sourcetree.build() + +class InstallCommand(SourceClassCommand): + name = 'install' + summary = 'install the built things' + def run(self): + self.bs_sourcetree.install() + + +class MakeCommand(SourceClassCommand): + name = 'make' + summary = 'run make in builddir' + def validate_args(self, *args, **kwargs): + pass + def run(self): + os.chdir(self.bs_sourcetree.config.builddir) + prog_run(["make"] + list(self.args), + self.context) + + +class ConfigCommand(SourceClassCommand): + name = 'config' + summary = 'set/get config values' + usage = '(srcdir|builddir|installdir)' + + def validate_args(self, *args, **kwargs): + items = ('srcdir', 'builddir', 'installdir', ) + if len(args) == 0: + raise CommandLineError("'%s' command requires at least one parameter (%s)", + self.name, ', '.join(items)) + elif len(args) == 1 and args[0] in items: + pass + elif len(args) == 2 and args[0] in items: + if args[0] in ('srcdir', ): + raise CommandLineError("'%s' command cannot change 'srcdir'", + self.name) + else: + pass + else: + raise CommandLineError("'%s' requires less or different parameters", + self.name) + + def run(self): + git_get_items = ('builddir', 'installdir', 'srcdir') + git_set_items = ('builddir', 'installdir', ) + if len(self.args) == 1: + if self.args[0] in git_get_items: + print getattr(self.vcs_sourcetree.config, self.args[0]) + else: + assert(False) + elif len(self.args) == 2: + if self.args[0] == 'builddir': + self.vcs_sourcetree.config.builddir = self.args[1] + elif self.args[0] == 'installdir': + self.vcs_sourcetree.config.installdir = self.args[1] + else: + assert(False) + else: + assert(False) + + +######################################################################## +# Commands +######################################################################## + +class NBB_Command(object): + def __init__(self, cmd, cmdargs, context): + if Command.plugins.has_key(cmd): + try: + c = Command.plugins[cmd](*cmdargs, **{'context':context}) + c.run() + except CommandLineError, e: + print "%(prog)s: Fatal:" % context, e + sys.exit(2) + except ProgramRunError, e: + print "%(prog)s: Fatal:" % context, e + print "Program aborted." + else: + print "Fatal: Unknown command '%s'" % cmd + raise NotImplementedError() + diff --git a/src/nbblib/main.py b/src/nbblib/main.py new file mode 100644 index 0000000..3014355 --- /dev/null +++ b/src/nbblib/main.py @@ -0,0 +1,229 @@ +######################################################################## +# Main program +######################################################################## + + +"""\ +nbb (ndim's branch builder) %(PACKAGE_VERSION)s +Build, install given branch of source code into a branch specific place +Copyright (C) 2007, 2008 Hans Ulrich Niedermann <hun@n-dimensional.de> +TBA: License conditions + +Usage: %(prog)s <to-be-determined> + +Features: + * supports git branches + * supports bzr branches (requires useful branch nick, TBD: bzr config ?) + * does out-of-source-tree builds (in-source-tree-builds unsupported) + * direct support for automake/autoconf based build systems + * TBD: supports execution of user commands in source, build, install dirs + +DONE: + * VCS config support ('git config', etc.) + * Build system support: automake/autoconf + +TODO: (Large list) + * Build system support: cmake, scons, ... + * Fine-tune init, configure, build, install commands with knowledge + gained with git-amb, especially the command interdependencies. + * implement *-sh and *-run commands + * General removal of redundancy in Python code. + * More declarative syntax elements in the Python code. + * Use declarations for command line parsing, and help text generation. + * Add global --nick or similar option to determine the branch + name to use for composing the pathes. + * Store config in ${srcdir}/.nbb.conf instead of 'git config'? + More portable. bzr does not have a config interface, for example. + * Model different "stages" of e.g. automake builds as distinct objects, + including proper dependency detectors, and stuff? OK, we're not going + to duplicate scons here. + * Design nice user interface. Requirements: + * print top_srcdir, builddir, installdir. OK: 'config' + * start subshell in top_srcdir, builddir, installdir + * run 'autoreconf' type step. OK: 'init' + * run 'configure' type step. OK: 'configure' + * run 'make' type step. OK: 'build' + * run 'make install' type step. OK: 'install' + * run custom (make) commands. OK: 'make' + * Bash syntax completion for that user interface. + * Man page or something similar. Generate from help texts? + +TBD: Command line interface: + + Run default build commands: + $ %(prog)s [general options] init [command specific options] + $ %(prog)s [general options] configure [command specific options] + $ %(prog)s [general options] build [command specific options] + $ %(prog)s [general options] install [command specific options] + + Run cleanup commands: + $ %(prog)s [general options] purge [command specific options] + Command specific options: + --builddir-only + --installdir-only + $ %(prog)s [general options] purge-all # either this + $ %(prog)s [general options] purge --all # or that + TBD: 'make clean', 'make distclean' and similar stuff? + + Get/set config: + $ %(prog)s [general options] config srcdir + $ %(prog)s [general options] config builddir [<builddir>] + $ %(prog)s [general options] config installdir [<installdir>] + + Start an interactive shell in either of the three directories: + $ %(prog)s [general options] src-sh [command specific options] + $ %(prog)s [general options] build-sh [command specific options] + $ %(prog)s [general options] install-sh [command specific options] + + Run command in builddir: + $ %(prog)s [general options] run <command> [<param>...] + $ %(prog)s [general options] run [command specific options... <-->] <cmd>... + + (Not sure about these) + Run a non-interactive shell command in either of the three directories: + $ %(prog)s [general options] src-sh [command specific options] <command>... + $ %(prog)s [general options] build-sh [command specific options] <command>... + $ %(prog)s [general options] install-sh [command specific options] <cmd...> + +Global options: + + -h --help Print this help text + -V --version Print program version number + + -n --dry-run Do not actually execute any commands + + -b --build-system Force buildsystem detection (%(buildsystems)s) + -v --vcs Force VCS detection (%(vcssystems)s) +""" + + +from nbblib.bs import * +from nbblib.commands import * +from nbblib.package import * +from nbblib.vcs import * + + +def print_version(context): + print "%(prog)s (ndim's branch builder) %(PACKAGE_VERSION)s" % context + + +def print_help(context): + print __doc__ % context, + + +class Property(object): + def __init__(self, **kwargs): + if kwargs.has_key('default'): + self.default = kwargs['default'] + def __get__(self, instance, owner): + if hasattr(self, 'value'): + return self.value + elif hasattr(self, 'default'): + return self.default + else: + return None + def __set__(self, instance, value): + if hasattr(self, 'value'): + raise "Property cannot be set more than once" + elif not self.isvalid(value): + raise "Property cannot be set to invalid value '%s'" % value + else: + self.value = self.convert(value) + def __str__(self): + if hasattr(self, 'value'): + return self.value + else: + return '<undefined property>' + def isvalid(self, value): + return True + def convert(self, value): + return value + + +class ProgProperty(Property): + def convert(self, value): + prog = value + idx = prog.rfind('/') + if idx >= 0: + prog = prog[idx+1:] + return prog + + +class VCSProperty(Property): + def isvalid(self, value): + return (value in VCSourceTree.plugins.keys()) + + +class BSProperty(Property): + def isvalid(self, value): + return (value in BSSourceTree.plugins.keys()) + + +class BoolProperty(Property): + def __init__(self, default=False): + super(BoolProperty, self).__init__(default=default) + def isvalid(self, value): + return (value in (True, False)) + +class DryRunProperty(BoolProperty): + def __init__(self): + super(DryRunProperty, self).__init__(default=False) + + +class Context(object): + PACKAGE_VERSION = Property() + prog = ProgProperty() + vcs = VCSProperty() + bs = BSProperty() + dry_run = DryRunProperty() + verbose = BoolProperty() + vcssystems = Property() + buildsystems = Property() + + def __getitem__(self, key): + """emulate a dict() for the purpose of "%(prog)s" % context""" + return getattr(self, key) + + +def main(argv): + context = Context() + context.PACKAGE_VERSION = PACKAGE_VERSION + context.vcssystems = ", ".join(VCSourceTree.plugins.keys()) + context.buildsystems = ", ".join(BSSourceTree.plugins.keys()) + context.prog = argv[0] + + if len(argv) < 2: + print "Fatal: %(prog)s requires some arguments" % context + return 2 + + i = 1 + while i<len(argv): + if argv[i][0] != '-': + break + if argv[i] in ('-h', '--help'): + print_help(context) + return + elif argv[i] in ('-V', '--version'): + print_version(context) + return + elif argv[i] in ('-n', '--dry-run'): + context.dry_run = True + elif argv[i] in ('-b', '--build-system'): + i = i + 1 + assert(i<len(argv)) + context.bs = argv[i] + elif argv[i][:6] == '--build-system=': + context.bs = argv[i][6:] + elif argv[i] in ('-v', '--vcs'): + i = i + 1 + assert(i<len(argv)) + context.vcs = argv[i] + elif argv[i][:6] == '--vcs=': + context.vcs = argv[i][6:] + elif argv[i] in ('--verbose', ): + context.verbose = True + # print "", i, argv[i] + i = i + 1 + cmd = argv[i] + cmdargs = argv[i+1:] + nbb = NBB_Command(cmd, cmdargs, context=context) diff --git a/src/nbblib/package.in b/src/nbblib/package.in new file mode 100644 index 0000000..240bfd6 --- /dev/null +++ b/src/nbblib/package.in @@ -0,0 +1,8 @@ +# Used to make sure nbb_lib and nbb fit together +PACKAGE_VERSION = "@PACKAGE_VERSION@" +GIT_CONFIG_PREFIX = 'nbb' + +# vim: syntax=python +# Local Variables: +# mode: python +# End: diff --git a/src/nbblib/plugins.py b/src/nbblib/plugins.py new file mode 100644 index 0000000..bd6839b --- /dev/null +++ b/src/nbblib/plugins.py @@ -0,0 +1,54 @@ +class DuplicatePluginName(Exception): + pass + + +class PluginDict(object): + """Helper for GenericPluginMeta class + + Behaves basically like a standard dict, but will raise an exception + when asked to update an existing value. + """ + def __init__(self): + self.dict = {} + def __getitem__(self, *args): + return self.dict.__getitem__(*args) + def __setitem__(self, key, value): + if self.dict.has_key(key): + raise DuplicatePluginName() + else: + self.dict[key] = value + def items(self): return self.dict.items() + def keys(self): return self.dict.keys() + def values(self): return self.dict.values() + def __iter__(self): return self.dict.__iter__() + def __str__(self): return self.dict.__str__() + def __repr__(self): return self.dict.__repr__() + def __len__(self): return self.dict.__len__() + def has_key(self, key): return self.dict.has_key(key) + + +######################################################################## +# Generic plugin system +######################################################################## +# Plugin architecture (metaclass tricks) by Marty Alchin from +# http://gulopine.gamemusic.org/2008/jan/10/simple-plugin-framework/ +# Slightly modified go store plugins as dict. +######################################################################## + +class GenericPluginMeta(type): + def __init__(cls, name, bases, attrs): + if not hasattr(cls, 'plugins'): + # This branch only executes when processing the mount point itself. + # So, since this is a new plugin type, not an implementation, this + # class shouldn't be registered as a plugin. Instead, it sets up a + # list where plugins can be registered later. + cls.plugins = PluginDict() + elif hasattr(cls, 'name'): + # This must be a plugin implementation, which should be registered. + # Simply appending it to the list is all that's needed to keep + # track of it later. + cls.plugins[cls.name] = cls + else: + # This must be an abstract subclass of plugins. + pass + diff --git a/src/nbblib/progutils.py b/src/nbblib/progutils.py new file mode 100644 index 0000000..456405e --- /dev/null +++ b/src/nbblib/progutils.py @@ -0,0 +1,55 @@ +######################################################################## +# Utility functions +######################################################################## + + +import os +import subprocess + + +def prog_stdout(call_list): + """Run program and return stdout (similar to shell backticks)""" + p = subprocess.Popen(call_list, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = p.communicate(input=None) + return stdout.strip() + + +def prog_retstd(call_list): + """Run program and return stdout (similar to shell backticks)""" + p = subprocess.Popen(call_list, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = p.communicate(input=None) + return (p.returncode, stdout.strip(), stderr.strip()) + + +class ProgramRunError(Exception): + """A program run returns a retcode != 0""" + def __init__(self, call_list, retcode, cwd=None): + self.call_list = call_list + self.retcode = retcode + if cwd: + self.cwd = cwd + else: + self.cwd = os.getcwd() + def __str__(self): + return ("Error running program (%s, retcode=%d, cwd=%s)" + % (repr(self.call_list), + self.retcode, + repr(self.cwd))) + + +def prog_run(call_list, context): + """Run program showing its output. Raise exception if retcode != 0.""" + print "RUN:", call_list + print " in", os.getcwd() + if context.dry_run: + return None + p = subprocess.Popen(call_list) + stdout, stderr = p.communicate(input=None) + if p.returncode != 0: + raise ProgramRunError(call_list, p.returncode, os.getcwd()) + return p.returncode + diff --git a/src/nbblib/vcs.py b/src/nbblib/vcs.py new file mode 100644 index 0000000..b0aee5b --- /dev/null +++ b/src/nbblib/vcs.py @@ -0,0 +1,217 @@ +import os +import urlparse + + +from nbblib.package import * +from nbblib.plugins import * +from nbblib.progutils import * + + +class AbstractConfig(object): + """Return static config until we implement real config reading""" + + def __init__(self, srcdir, nick): + super(AbstractConfig, self).__init__() + self._srcdir = srcdir + self._nick = nick + + def get_srcdir(self): + return os.path.join(self._srcdir) + srcdir = property(get_srcdir) + + def get_builddir(self): + return os.path.join(self._srcdir, "_build", self._nick) + builddir = property(get_builddir) + + def get_installdir(self): + return os.path.join(self._srcdir, "_install", self._nick) + installdir = property(get_installdir) + + +######################################################################## +# VCS Source Tree plugin system +######################################################################## + +class NotAVCSourceTree(Exception): + pass + + +class AmbigousVCSource(Exception): + def __init__(self, srcdir, matches): + super(AmbigousVCSource, self).__init__() + self.srcdir = srcdir + self.matches = matches + def __str__(self): + fmt = " %-9s %-15s %s" + def strmatch(m): + return fmt % (m.name, m.branch_name(), m.tree_root()) + alist = ([fmt % ('VCS Type', 'Branch Name', 'Source tree root')] + + map(strmatch, self.matches)) + return ("More than one source tree VCS type detected for '%s':\n#%s" + % (self.srcdir, '\n '.join(alist))) + + +class VCSourceTree(object): + """ + Mount point for plugins which refer to actions that can be performed. + + Plugins implementing this reference should provide the following + interface: + + name attribute + The text to be displayed, describing the version control system + __init__ function + Must raise NotAVCSourceTree() if it is not a VCS source tree + """ + __metaclass__ = GenericPluginMeta + + def __init__(self, context): + super(VCSourceTree, self).__init__() + self.context = context + + @classmethod + def detect(cls, srcdir, context): + """Detect VCS tree type and return object representing it""" + if len(VCSourceTree.plugins) < 1: + raise "No VC source tree classes registered" + matches = PluginDict() + for key, klass in VCSourceTree.plugins.items(): + try: + t = klass(srcdir, context) + if t.tree_root() == srcdir: + matches[key] = t + except NotAVCSourceTree, e: + pass + if len(matches) > 1: + raise AmbigousVCSource(srcdir, matches.values()) + elif len(matches) < 1: + raise NotAVCSourceTree(srcdir) + return matches[matches.keys()[0]] + + def get_config(self): + """Get configuration object which determines builddir etc""" + return AbstractConfig(self.tree_root(), self.branch_name()) + config = property(get_config) + + def tree_root(self): + """Get absolute path to source tree root""" + raise NotImplementedError() + + def branch_name(self): + """Return name identifying the branch""" + raise NotImplementedError() + + def __str__(self): + return repr(self) + + def __repr__(self): + return "<%s(%s, %s)>" % (self.__class__.__name__, + repr(self.tree_root()), + repr(self.branch_name())) + + +######################################################################## +# VCS Source Tree plugins +######################################################################## + +class GitSourceTree(VCSourceTree): + + name = 'git' + + def __init__(self, srcdir, context): + super(GitSourceTree, self).__init__(context) + os.chdir(srcdir) + if "true" != prog_stdout(["git", "rev-parse", + "--is-inside-work-tree"]): + raise NotAVCSourceTree() + reldir = prog_stdout(["git", "rev-parse", "--show-cdup"]) + if reldir: + os.chdir(reldir) + self.__tree_root = os.getcwd() + + def get_config(self): + return GitConfig(self.tree_root(), self.branch_name()) + config = property(get_config) + + def tree_root(self): + return self.__tree_root + + def branch_name(self): + bname = prog_stdout(["git", "symbolic-ref", "HEAD"]) + refs,heads,branch = bname.split('/') + assert(refs=='refs' and heads=='heads') + return branch + + +class GitConfig(AbstractConfig): + """git config interface""" + + def __init__(self, *args, **kwargs): + super(GitConfig, self).__init__(*args, **kwargs) + + def _itemname(self, item): return '.'.join((GIT_CONFIG_PREFIX, item, )) + def _myreldir(self, rdir): + return os.path.join(self._srcdir, rdir, self._nick) + + def get_builddir(self): + ret, stdout, stderr = prog_retstd(['git', 'config', self._itemname('builddir')]) + assert(stderr == "") + if ret == 0 and stdout: + return self._myreldir(stdout) + else: + return super(GitConfig, self).get_builddir() + + def set_builddir(self, value): + ret, stdout, stderr = prog_retstd(['git', 'config', self._itemname('builddir'), value]) + assert(ret == 0 and stdout == "" and stderr == "") + + builddir = property(get_builddir, set_builddir) + + def get_installdir(self): + ret, stdout, stderr = prog_retstd(['git', 'config', self._itemname('installdir')]) + assert(stderr == "") + if ret == 0 and stdout: + return self._myreldir(stdout) + else: + return super(GitConfig, self).get_installdir() + + def set_installdir(self, value): + ret, stdout, stderr = prog_retstd(['git', 'config', self._itemname('installdir'), value]) + assert(ret == 0 and stdout == "" and stderr == "") + + installdir = property(get_installdir, set_installdir) + + +class BzrSourceTree(VCSourceTree): + + name = 'bzr' + + def __init__(self, srcdir, context): + super(BzrSourceTree, self).__init__(context) + try: + import bzrlib.workingtree + wt,b = bzrlib.workingtree.WorkingTree.open_containing(srcdir) + except bzrlib.errors.NotBranchError: + raise NotAVCSourceTree() + except ImportError: + raise NotAVCSourceTree() + self.wt = wt + #print "wt:", wt + #print "wt:", dir(wt) + #print "wt.branch:", wt.branch + #print "wt.branch:", dir(wt.branch) + #print "wt.branch.nick:", wt.branch.nick + #print "wt.branch.abspath:", wt.branch.abspath + #print "wt.branch.base:", wt.branch.base + #print "wt.branch.basis_tree:", wt.branch.basis_tree() + + def tree_root(self): + proto,host,path,some,thing = urlparse.urlsplit(self.wt.branch.base) + assert(proto == "file" and host == "") + assert(some == "" and thing == "") + return os.path.abspath(path) + + def branch_name(self): + return self.wt.branch.nick + + |