diff options
author | Frederic Lepied <flepied@mandriva.com> | 2005-12-07 10:06:33 +0000 |
---|---|---|
committer | Frederic Lepied <flepied@mandriva.com> | 2005-12-07 10:06:33 +0000 |
commit | 2c78c40c9bfae22d1501022b2e52cf938b5a957e (patch) | |
tree | 33a030236dd735f665e7b1ff1a9fba203742ecc0 | |
parent | aa9174bf0cce8d6a3c6e4d9e795be717da184406 (diff) | |
download | mgarepo-topic/V1_5_X@819.tar mgarepo-topic/V1_5_X@819.tar.gz mgarepo-topic/V1_5_X@819.tar.bz2 mgarepo-topic/V1_5_X@819.tar.xz mgarepo-topic/V1_5_X@819.zip |
Initial revisionR1_5_3_1-4mdktopic/V1_5_X@821topic/V1_5_X@819topic/V1_5_3@959topic/V1_5_3@819topic/V1_5_3
34 files changed, 3640 insertions, 0 deletions
diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..9311d09 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +recursive-include RepSys *.py +include repsys repsys.conf MANIFEST.in diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..59af750 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: repsys +Version: 1.5.2 +Summary: Tools for Mandriva Linux repository access and management +Home-page: http://qa.mandrakesoft.com/twiki/bin/view/Main/RepositorySystem +Author: Gustavo Niemeyer +Author-email: gustavo@niemeyer.net +License: GPL +Description: Tools for Mandriva Linux repository access and management. +Platform: UNKNOWN diff --git a/RepSys/ConfigParser.py b/RepSys/ConfigParser.py new file mode 100644 index 0000000..0f219b9 --- /dev/null +++ b/RepSys/ConfigParser.py @@ -0,0 +1,398 @@ +""" +This is a heavily hacked version of ConfigParser to keep the order in +which options and sections are read, and allow multiple options with +the same key. +""" +from __future__ import generators +import string, types +import re + +__all__ = ["NoSectionError","DuplicateSectionError","NoOptionError", + "InterpolationError","InterpolationDepthError","ParsingError", + "MissingSectionHeaderError","ConfigParser", + "MAX_INTERPOLATION_DEPTH"] + +DEFAULTSECT = "DEFAULT" + +MAX_INTERPOLATION_DEPTH = 10 + +# exception classes +class Error(Exception): + def __init__(self, msg=''): + self._msg = msg + Exception.__init__(self, msg) + def __repr__(self): + return self._msg + __str__ = __repr__ + +class NoSectionError(Error): + def __init__(self, section): + Error.__init__(self, 'No section: %s' % section) + self.section = section + +class DuplicateSectionError(Error): + def __init__(self, section): + Error.__init__(self, "Section %s already exists" % section) + self.section = section + +class NoOptionError(Error): + def __init__(self, option, section): + Error.__init__(self, "No option `%s' in section: %s" % + (option, section)) + self.option = option + self.section = section + +class InterpolationError(Error): + def __init__(self, reference, option, section, rawval): + Error.__init__(self, + "Bad value substitution:\n" + "\tsection: [%s]\n" + "\toption : %s\n" + "\tkey : %s\n" + "\trawval : %s\n" + % (section, option, reference, rawval)) + self.reference = reference + self.option = option + self.section = section + +class InterpolationDepthError(Error): + def __init__(self, option, section, rawval): + Error.__init__(self, + "Value interpolation too deeply recursive:\n" + "\tsection: [%s]\n" + "\toption : %s\n" + "\trawval : %s\n" + % (section, option, rawval)) + self.option = option + self.section = section + +class ParsingError(Error): + def __init__(self, filename): + Error.__init__(self, 'File contains parsing errors: %s' % filename) + self.filename = filename + self.errors = [] + + def append(self, lineno, line): + self.errors.append((lineno, line)) + self._msg = self._msg + '\n\t[line %2d]: %s' % (lineno, line) + +class MissingSectionHeaderError(ParsingError): + def __init__(self, filename, lineno, line): + Error.__init__( + self, + 'File contains no section headers.\nfile: %s, line: %d\n%s' % + (filename, lineno, line)) + self.filename = filename + self.lineno = lineno + self.line = line + +class ConfigParser: + def __init__(self, defaults=None): + # Options are stored in __sections_list like this: + # [(sectname, [(optname, optval), ...]), ...] + self.__sections_list = [] + self.__sections_dict = {} + if defaults is None: + self.__defaults = {} + else: + self.__defaults = defaults + + def defaults(self): + return self.__defaults + + def sections(self): + return self.__sections_dict.keys() + + def has_section(self, section): + return self.__sections_dict.has_key(section) + + def options(self, section): + self.__sections_dict[section] + try: + opts = self.__sections_dict[section].keys() + except KeyError: + raise NoSectionError(section) + return self.__defaults.keys()+opts + + def read(self, filenames): + if type(filenames) in types.StringTypes: + filenames = [filenames] + for filename in filenames: + try: + fp = open(filename) + except IOError: + continue + self.__read(fp, filename) + fp.close() + + def readfp(self, fp, filename=None): + if filename is None: + try: + filename = fp.name + except AttributeError: + filename = '<???>' + self.__read(fp, filename) + + def set(self, section, option, value): + if self.__sections_dict.has_key(section): + sectdict = self.__sections_dict[section] + sectlist = [] + self.__sections_list.append((section, sectlist)) + elif section == DEFAULTSECT: + sectdict = self.__defaults + sectlist = None + else: + sectdict = {} + self.__sections_dict[section] = sectdict + sectlist = [] + self.__sections_list.append((section, sectlist)) + xform = self.optionxform(option) + sectdict[xform] = value + if sectlist is not None: + sectlist.append([xform, value]) + + def get(self, section, option, raw=0, vars=None): + d = self.__defaults.copy() + try: + d.update(self.__sections_dict[section]) + except KeyError: + if section != DEFAULTSECT: + raise NoSectionError(section) + if vars: + d.update(vars) + option = self.optionxform(option) + try: + rawval = d[option] + except KeyError: + raise NoOptionError(option, section) + if raw: + return rawval + return self.__interpolate(rawval, d) + + def getall(self, section, option, raw=0, vars=None): + option = self.optionxform(option) + values = [] + d = self.__defaults.copy() + if section != DEFAULTSECT: + for sectname, options in self.__sections_list: + if sectname == section: + for optname, value in options: + if optname == option: + values.append(value) + d[optname] = value + if raw: + return values + if vars: + d.update(vars) + for i in len(values): + values[i] = self.__interpolate(values[i], d) + return values + + def walk(self, section, option=None, raw=0, vars=None): + # Build dictionary for interpolation + try: + d = self.__sections_dict[section].copy() + except KeyError: + if section == DEFAULTSECT: + d = {} + else: + raise NoSectionError(section) + d.update(self.__defaults) + if vars: + d.update(vars) + + # Start walking + if option: + option = self.optionxform(option) + if section != DEFAULTSECT: + for sectname, options in self.__sections_list: + if sectname == section: + for optname, value in options: + if not option or optname == option: + if not raw: + value = self.__interpolate(value, d) + yield (optname, value) + + def __interpolate(self, value, vars): + rawval = value + depth = 0 + while depth < 10: + depth = depth + 1 + if value.find("%(") >= 0: + try: + value = value % vars + except KeyError, key: + raise InterpolationError(key, option, section, rawval) + else: + break + if value.find("%(") >= 0: + raise InterpolationDepthError(option, section, rawval) + return value + + def __get(self, section, conv, option): + return conv(self.get(section, option)) + + def getint(self, section, option): + return self.__get(section, string.atoi, option) + + def getfloat(self, section, option): + return self.__get(section, string.atof, option) + + def getboolean(self, section, option): + states = {'1': 1, 'yes': 1, 'true': 1, 'on': 1, + '0': 0, 'no': 0, 'false': 0, 'off': 0} + v = self.get(section, option) + if not states.has_key(v.lower()): + raise ValueError, 'Not a boolean: %s' % v + return states[v.lower()] + + def optionxform(self, optionstr): + #return optionstr.lower() + return optionstr + + def has_option(self, section, option): + """Check for the existence of a given option in a given section.""" + if not section or section == "DEFAULT": + return self.__defaults.has_key(option) + elif not self.has_section(section): + return 0 + else: + option = self.optionxform(option) + return self.__sections_dict[section].has_key(option) + + SECTCRE = re.compile(r'\[(?P<header>[^]]+)\]') + OPTCRE = re.compile(r'(?P<option>\S+)\s*(?P<vi>[:=])\s*(?P<value>.*)$') + + def __read(self, fp, fpname): + cursectdict = None # None, or a dictionary + optname = None + lineno = 0 + e = None # None, or an exception + while 1: + line = fp.readline() + if not line: + break + lineno = lineno + 1 + # comment or blank line? + if line.strip() == '' or line[0] in '#;': + continue + if line.split()[0].lower() == 'rem' \ + and line[0] in "rR": # no leading whitespace + continue + # continuation line? + if line[0] in ' \t' and cursectdict is not None and optname: + value = line.strip() + if value: + k = self.optionxform(optname) + cursectdict[k] = "%s\n%s" % (cursectdict[k], value) + cursectlist[-1][1] = "%s\n%s" % (cursectlist[-1][1], value) + # a section header or option header? + else: + # is it a section header? + mo = self.SECTCRE.match(line) + if mo: + sectname = mo.group('header') + if self.__sections_dict.has_key(sectname): + cursectdict = self.__sections_dict[sectname] + cursectlist = [] + self.__sections_list.append((sectname, cursectlist)) + elif sectname == DEFAULTSECT: + cursectdict = self.__defaults + cursectlist = None + else: + cursectdict = {} + self.__sections_dict[sectname] = cursectdict + cursectlist = [] + self.__sections_list.append((sectname, cursectlist)) + # So sections can't start with a continuation line + optname = None + # no section header in the file? + elif cursectdict is None: + raise MissingSectionHeaderError(fpname, lineno, `line`) + # an option line? + else: + mo = self.OPTCRE.match(line) + if mo: + optname, vi, optval = mo.group('option', 'vi', 'value') + if vi in ('=', ':') and ';' in optval: + # ';' is a comment delimiter only if it follows + # a spacing character + pos = optval.find(';') + if pos and optval[pos-1] in string.whitespace: + optval = optval[:pos] + optval = optval.strip() + # allow empty values + if optval == '""': + optval = '' + xform = self.optionxform(optname) + cursectdict[xform] = optval + if cursectlist is not None: + cursectlist.append([xform, optval]) + else: + # a non-fatal parsing error occurred. set up the + # exception but keep going. the exception will be + # raised at the end of the file and will contain a + # list of all bogus lines + if not e: + e = ParsingError(fpname) + e.append(lineno, `line`) + # if any parsing errors occurred, raise an exception + if e: + raise e + +# Here we wrap this hacked ConfigParser into something more useful +# for us. + +import os + +class Config: + def __init__(self): + self._config = ConfigParser() + conffiles = [] + conffiles.append("/etc/repsys.conf") + repsys_conf = os.environ.get("REPSYS_CONF") + if repsys_conf: + conffiles.append(repsys_conf) + conffiles.append(os.path.expanduser("~/.repsys/config")) + for file in conffiles: + if os.path.isfile(file): + self._config.read(file) + + def sections(self): + try: + return self._config.sections() + except Error: + return [] + + def options(self, section): + try: + return self._config.options(section) + except Error: + return [] + + def set(self, section, option, value): + return self._config.set(section, option, value) + + def walk(self, section): + return self._config.walk(section) + + def get(self, section, option, default=None): + try: + return self._config.get(section, option) + except Error: + return default + + def getint(self, section, option, default=None): + ret = self.get(section, option, default) + if type(ret) == type(""): + return int(ret) + + def getbool(self, section, option, default=None): + ret = self.get(section, option, default) + states = {'1': 1, 'yes': 1, 'true': 1, 'on': 1, + '0': 0, 'no': 0, 'false': 0, 'off': 0} + if type(ret) == type("") and states.has_key(ret.lower()): + return states[ret.lower()] + return default + +# vim:ts=4:sw=4:et diff --git a/RepSys/__init__.py b/RepSys/__init__.py new file mode 100644 index 0000000..c371e7a --- /dev/null +++ b/RepSys/__init__.py @@ -0,0 +1,9 @@ +#!/usr/bin/python + +import ConfigParser +config = ConfigParser.Config() +del ConfigParser + +class Error(Exception): pass + +# vim:et:ts=4:sw=4 diff --git a/RepSys/cgi/__init__.py b/RepSys/cgi/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/RepSys/cgi/__init__.py diff --git a/RepSys/cgi/soapserver.py b/RepSys/cgi/soapserver.py new file mode 100644 index 0000000..2f6b751 --- /dev/null +++ b/RepSys/cgi/soapserver.py @@ -0,0 +1,93 @@ +#!/usr/bin/python +from RepSys import Error, config +from RepSys.rpmutil import get_srpm +from RepSys.cgiutil import CgiError, get_targets +import sys +import os + +try: + import NINZ.dispatch +except ImportError: + NINZ = None + +class SoapIface: + def author_email(self, author): + return config.get("users", author) + + def submit_package(self, packageurl, packagerev, targetname): + username = os.environ.get("REMOTE_USER") + packager = config.get("users", username) + if not packager: + raise CgiError, "your email was not found" + elif not packagerev: + raise CgiError, "no revision provided" + elif not targetname: + raise CgiError, "no target provided" + else: + targetname = targetname.lower() + for target in get_targets(): + if target.name.lower() == targetname: + break + else: + raise CgiError, "target not found" + try: + tmp = int(packagerev) + except ValueError: + raise CgiError, "invalid revision provided" + for allowed in target.allowed: + if packageurl.startswith(allowed): + break + else: + raise CgiError, "%s is not allowed for this target" \ + % packageurl + get_srpm(packageurl, + revision=packagerev, + targetdirs=target.target, + packager=packager, + revname=1, + svnlog=1, + scripts=target.scripts) + return 1 + + def submit_targets(self): + return [x.name for x in get_targets()] + +TEMPLATE = """\ +Content-type: text/html + +<html> +<head> +<title>Repository system SOAP server</title> +</head> +<body bgcolor="white"> +<br> +<hr> +<center> +<b>%(message)s</b> +</center> +<hr> +</body> +</html> +""" + +def show(msg="", error=0): + if error: + msg = '<font color="red">%s</font>' % msg + print TEMPLATE % {"message":msg} + +def main(): + if not os.environ.has_key('REQUEST_METHOD'): + sys.stderr.write("error: this program is meant to be used as a cgi\n") + sys.exit(1) + if not NINZ: + show("NINZ is not properly installed in this system", error=1) + sys.exit(1) + username = os.environ.get("REMOTE_USER") + method = os.environ.get("REQUEST_METHOD") + if not username or method != "POST": + show("This is a SOAP interface!", error=1) + sys.exit(1) + + NINZ.dispatch.AsCGI(modules=(SoapIface(),)) + +# vim:et:ts=4:sw=4 diff --git a/RepSys/cgi/submit.py b/RepSys/cgi/submit.py new file mode 100644 index 0000000..10f7cb2 --- /dev/null +++ b/RepSys/cgi/submit.py @@ -0,0 +1,119 @@ +#!/usr/bin/python +from RepSys import Error, config +from RepSys.rpmutil import get_srpm +from RepSys.cgiutil import CgiError, get_targets +import cgi +import sys +import os + +TEMPLATE = """\ +<html> +<head> +<title>Repository package submission system</title> +</head> +<body bgcolor="white"> +<table cellspacing=0 cellpadding=0 border=0 width="100%%"> + <tr bgcolor="#020264"><td align="left" valign="middle"><img src="http://qa.mandriva.com/mandriva.png" hspace=0 border=0 alt=""></td></tr> +</table> +<br> +<hr> +<center> +<b>%(message)s</b> +<br><br> +<form method="POST" action=""> +<table><tr><td valign="top"> + Package URL:<br> + <input name="packageurl" size="60" value="svn+ssh://cvs.mandriva.com/svn/mdv/cooker/"><br> + <small>Ex. svn+ssh://cvs.mandriva.com/svn/mdv/cooker/pkgname</small><br> + </td><td valign="top"> + Revision:<br> + <input name="packagerev" size="10" value=""><br> + </td></tr></table> + <br> + Package target:<br> + <select name="target" size=5> + %(targetoptions)s + </select><br> + <br> + <input type="submit" value="Submit package"> +</form> +</center> +<hr/> +</body> +</html> +""" + +def get_targetoptions(): + s = "" + selected = " selected" + for target in get_targets(): + s += '<option value="%s"%s>%s</option>' \ + % (target.name, selected, target.name) + selected = "" + return s + +def show(msg="", error=0): + if error: + msg = '<font color="red">%s</font>' % msg + print TEMPLATE % {"message":msg, "targetoptions":get_targetoptions()} + +def submit_packages(packager): + form = cgi.FieldStorage() + packageurl = form.getfirst("packageurl", "").strip() + packagerev = form.getfirst("packagerev", "").strip() + if not packageurl: + show() + elif not packagerev: + raise CgiError, "No revision provided!" + else: + targetname = form.getfirst("target") + if not targetname: + raise CgiError, "No target selected!" + for target in get_targets(): + if target.name == targetname: + break + else: + raise CgiError, "Target not found!" + try: + tmp = int(packagerev) + except ValueError: + raise CgiError, "Invalid revision provided!" + for allowed in target.allowed: + if packageurl.startswith(allowed): + break + else: + raise CgiError, "%s is not allowed for this target!" % packageurl + get_srpm(packageurl, + revision=packagerev, + targetdirs=target.target, + packager=packager, + revname=1, + svnlog=1, + scripts=target.scripts) + show("Package submitted!") + +def main(): + if not os.environ.has_key('REQUEST_METHOD'): + sys.stderr.write("error: this program is meant to be used as a cgi\n") + sys.exit(1) + print "Content-type: text/html\n\n" + try: + username = os.environ.get("REMOTE_USER") + method = os.environ.get("REQUEST_METHOD") + if not username or method != "POST": + show() + else: + useremail = config.get("users", username) + if not useremail: + raise CgiError, \ + "Your email was not found. Contact the administrator!" + submit_packages(useremail) + except CgiError, e: + show(str(e), error=1) + except Error, e: + error = str(e) + show(error[0].upper()+error[1:], error=1) + except: + cgi.print_exception() + +# vim:et:ts=4:sw=4 diff --git a/RepSys/cgi/xmlrpcserver.py b/RepSys/cgi/xmlrpcserver.py new file mode 100644 index 0000000..e0851d1 --- /dev/null +++ b/RepSys/cgi/xmlrpcserver.py @@ -0,0 +1,111 @@ +#!/usr/bin/python +from RepSys import Error, config +from RepSys.rpmutil import get_srpm +from RepSys.cgiutil import CgiError, get_targets +import sys +import os + +import xmlrpclib, cgi + +class XmlRpcIface: + def author_email(self, author): + return config.get("users", author) + + def submit_package(self, packageurl, packagerev, targetname): + username = os.environ.get("REMOTE_USER") + packager = config.get("users", username) + if not packager: + raise CgiError, "your email was not found" + elif not packagerev: + raise CgiError, "no revision provided" + elif not targetname: + raise CgiError, "no target provided" + else: + targetname = targetname.lower() + for target in get_targets(): + if target.name.lower() == targetname: + break + else: + raise CgiError, "target not found" + try: + tmp = int(packagerev) + except ValueError: + raise CgiError, "invalid revision provided" + for allowed in target.allowed: + if packageurl.startswith(allowed): + break + else: + raise CgiError, "%s is not allowed for this target" \ + % packageurl + get_srpm(packageurl, + revision=packagerev, + targetdirs=target.target, + packager=packager, + revname=1, + svnlog=1, + scripts=target.scripts) + return 1 + + def submit_targets(self): + return [x.name for x in get_targets()] + +TEMPLATE = """\ +Content-type: text/html + +<html> +<head> +<title>Repository system SOAP server</title> +</head> +<body bgcolor="white"> +<br> +<hr> +<center> +<b>%(message)s</b> +</center> +<hr> +</body> +</html> +""" + +def show(msg="", error=0): + if error: + msg = '<font color="red">%s</font>' % msg + print TEMPLATE % {"message":msg} + +def main(): + if not os.environ.has_key('REQUEST_METHOD'): + sys.stderr.write("error: this program is meant to be used as a cgi\n") + sys.exit(1) + username = os.environ.get("REMOTE_USER") + method = os.environ.get("REQUEST_METHOD") + if not username or method != "POST": + show("This is a XMLRPC interface!", error=1) + sys.exit(1) + + iface = XmlRpcIface() + + response = "" + try: + form = cgi.FieldStorage() + parms, method = xmlrpclib.loads(form.value) + meth = getattr(iface, method) + response = (meth(*parms),) + except CgiError, e: + msg = str(e) + try: + msg = msg.decode("iso-8859-1") + except UnicodeError: + pass + response = xmlrpclib.Fault(1, msg) + except Exception, e: + msg = str(e) + try: + msg = msg.decode("iso-8859-1") + except UnicodeError: + pass + response = xmlrpclib.Fault(1, msg) + + sys.stdout.write("Content-type: text/xml\n\n") + sys.stdout.write(xmlrpclib.dumps(response, methodresponse=1)) + +# vim:et:ts=4:sw=4 diff --git a/RepSys/cgiutil.py b/RepSys/cgiutil.py new file mode 100644 index 0000000..ebb65c9 --- /dev/null +++ b/RepSys/cgiutil.py @@ -0,0 +1,42 @@ +#!/usr/bin/python +from RepSys import Error, config +from RepSys.svn import SVN +import time + +class CgiError(Error): pass + +class SubmitTarget: + def __init__(self): + self.name = "" + self.target = "" + self.allowed = [] + self.scripts = [] + +TARGETS = [] + +def get_targets(): + global TARGETS + if not TARGETS: + target = SubmitTarget() + targetoptions = {} + for option, value in config.walk("submit"): + if targetoptions.has_key(option): + TARGETS.append(target) + target = SubmitTarget() + targetoptions = {} + targetoptions[option] = 1 + if option == "name": + target.name = value + elif option == "target": + target.target = value.split() + elif option == "allowed": + target.allowed = value.split() + elif option == "scripts": + target.scripts = value.split() + else: + raise Error, "unknown [submit] option %s" % option + if targetoptions: + TARGETS.append(target) + return TARGETS + +# vim:et:ts=4:sw=4 diff --git a/RepSys/command.py b/RepSys/command.py new file mode 100644 index 0000000..8029e08 --- /dev/null +++ b/RepSys/command.py @@ -0,0 +1,53 @@ +#!/usr/bin/python +from RepSys import Error, config +import sys, os, urllib +import optparse + +__all__ = ["OptionParser", "do_command", "default_parent"] + +class CapitalizeHelpFormatter(optparse.IndentedHelpFormatter): + + def format_usage(self, usage): + return optparse.IndentedHelpFormatter \ + .format_usage(self, usage).capitalize() + + def format_heading(self, heading): + return optparse.IndentedHelpFormatter \ + .format_heading(self, heading).capitalize() + +class OptionParser(optparse.OptionParser): + + def __init__(self, usage=None, help=None, **kwargs): + if not "formatter" in kwargs: + kwargs["formatter"] = CapitalizeHelpFormatter() + optparse.OptionParser.__init__(self, usage, **kwargs) + self._overload_help = help + + def format_help(self, formatter=None): + if self._overload_help: + return self._overload_help + else: + return optparse.OptionParser.format_help(self, formatter) + + def error(self, msg): + raise Error, msg + +def do_command(parse_options_func, main_func): + try: + opt = parse_options_func() + main_func(**opt.__dict__) + except Error, e: + sys.stderr.write("error: %s\n" % str(e)) + sys.exit(1) + +def default_parent(url): + if url.find("://") == -1: + default_parent = config.get("global", "default_parent") + if not default_parent: + raise Error, "received a relative url, " \ + "but default_parent was not setup" + type, rest = urllib.splittype(default_parent) + url = type+':'+os.path.normpath(rest+'/'+url) + return url + +# vim:et:ts=4:sw=4 diff --git a/RepSys/commands/__init__.py b/RepSys/commands/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/RepSys/commands/__init__.py diff --git a/RepSys/commands/authoremail.py b/RepSys/commands/authoremail.py new file mode 100644 index 0000000..aee7b58 --- /dev/null +++ b/RepSys/commands/authoremail.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +from RepSys import Error, config +from RepSys.command import * +import sys +import getopt + +HELP = """\ +Usage: repsys authoremail [OPTIONS] AUTHOR + +Options: + -h Show this message + +Examples: + repsys authoremail john +""" + +def parse_options(): + parser = OptionParser(help=HELP) + opts, args = parser.parse_args() + if len(args) != 1: + raise Error, "invalid arguments" + opts.author = args[0] + return opts + +def print_author_email(author): + email = config.get("users", author) + if not email: + raise Error, "author not found" + print email + +def main(): + do_command(parse_options, print_author_email) + +# vim:et:ts=4:sw=4 diff --git a/RepSys/commands/changed.py b/RepSys/commands/changed.py new file mode 100644 index 0000000..c99f3ae --- /dev/null +++ b/RepSys/commands/changed.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +from RepSys import Error +from RepSys.command import * +from RepSys.rpmutil import check_changed +import getopt +import sys + +HELP = """\ +Usage: repsys changed [OPTIONS] URL + +Options: + -a Check all packages in given URL + -s Show differences + -h Show this message + +Examples: + repsys changed http://repos/svn/cnc/snapshot/foo + repsys changed -a http://repos/svn/cnc/snapshot +""" + +def parse_options(): + parser = OptionParser(help=HELP) + parser.add_option("-a", dest="all", action="store_true") + parser.add_option("-s", dest="show", action="store_true") + opts, args = parser.parse_args() + if len(args) != 1: + raise Error, "invalid arguments" + opts.url = default_parent(args[0]) + opts.verbose = 1 # Unconfigurable + return opts + +def main(): + do_command(parse_options, check_changed) + +# vim:et:ts=4:sw=4 diff --git a/RepSys/commands/co.py b/RepSys/commands/co.py new file mode 100644 index 0000000..0c9d2dc --- /dev/null +++ b/RepSys/commands/co.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +from RepSys import Error +from RepSys.command import * +from RepSys.rpmutil import checkout +import getopt +import sys + +HELP = """\ +Usage: repsys co [OPTIONS] URL [LOCALPATH] + +Options: + -r REV Revision to checkout + -h Show this message + +Examples: + repsys co http://repos/svn/cnc/snapshot/foo + repsys co http://repos/svn/cnc/snapshot/foo foo-pkg +""" + +def parse_options(): + parser = OptionParser(help=HELP) + parser.add_option("-r", dest="revision") + opts, args = parser.parse_args() + if len(args) not in (1, 2): + raise Error, "invalid arguments" + opts.url = default_parent(args[0]) + if len(args) == 2: + opts.path = args[1] + else: + opts.path = None + return opts + +def main(): + do_command(parse_options, checkout) + +# vim:et:ts=4:sw=4 diff --git a/RepSys/commands/create.py b/RepSys/commands/create.py new file mode 100644 index 0000000..56af1ef --- /dev/null +++ b/RepSys/commands/create.py @@ -0,0 +1,30 @@ +#!/usr/bin/python +from RepSys import Error +from RepSys.command import * +from RepSys.rpmutil import create_package +import getopt +import sys + +HELP = """\ +Usage: repsys create [OPTIONS] URL + +Options: + -h Show this message + +Examples: + repsys create http://repos/svn/cnc/snapshot/newpkg +""" + +def parse_options(): + parser = OptionParser(help=HELP) + opts, args = parser.parse_args() + if len(args) != 1: + raise Error, "invalid arguments" + opts.pkgdirurl = default_parent(args[0]) + opts.verbose = 1 # Unconfigurable + return opts + +def main(): + do_command(parse_options, create_package) + +# vim:et:ts=4:sw=4 diff --git a/RepSys/commands/editlog.py b/RepSys/commands/editlog.py new file mode 100644 index 0000000..1962ed8 --- /dev/null +++ b/RepSys/commands/editlog.py @@ -0,0 +1,38 @@ +#!/usr/bin/python +from RepSys import Error +from RepSys.command import * +from RepSys.svn import SVN +import re + +HELP = """\ +Usage: repsys editlog [OPTIONS] [URL] REVISION + +Options: + -h Show this message + +Examples: + repsys editlog 14800 + repsys editlog https://repos/svn/cnc/snapshot 14800 +""" + +def parse_options(): + parser = OptionParser(help=HELP) + opts, args = parser.parse_args() + if len(args) == 2: + pkgdirurl, revision = args + elif len(args) == 1: + pkgdirurl, revision = "", args[0] + else: + raise Error, "invalid arguments" + opts.pkgdirurl = default_parent(pkgdirurl) + opts.revision = re.compile(r".*?(\d+).*").sub(r"\1", revision) + return opts + +def editlog(pkgdirurl, revision): + svn = SVN() + svn.propedit("svn:log", pkgdirurl, rev=revision) + +def main(): + do_command(parse_options, editlog) + +# vim:et:ts=4:sw=4 diff --git a/RepSys/commands/getspec.py b/RepSys/commands/getspec.py new file mode 100644 index 0000000..1079a81 --- /dev/null +++ b/RepSys/commands/getspec.py @@ -0,0 +1,31 @@ +#!/usr/bin/python +from RepSys import Error +from RepSys.command import * +from RepSys.rpmutil import get_spec +import getopt +import sys + +HELP = """\ +Usage: repsys getspec [OPTIONS] REPPKGURL + +Options: + -t DIR Use DIR as target for spec file (default is ".") + -h Show this message + +Examples: + repsys getspec http://repos/svn/cnc/snapshot/foo +""" + +def parse_options(): + parser = OptionParser(help=HELP) + parser.add_option("-t", dest="targetdir", default=".") + opts, args = parser.parse_args() + if len(args) != 1: + raise Error, "invalid arguments" + opts.pkgdirurl = default_parent(args[0]) + return opts + +def main(): + do_command(parse_options, get_spec) + +# vim:et:ts=4:sw=4 diff --git a/RepSys/commands/getsrpm.py b/RepSys/commands/getsrpm.py new file mode 100644 index 0000000..f2def54 --- /dev/null +++ b/RepSys/commands/getsrpm.py @@ -0,0 +1,76 @@ +#!/usr/bin/python +# +# This program will extract given version/revision of the named package +# from the Conectiva Linux repository system. +# +from RepSys import Error, config +from RepSys.command import * +from RepSys.rpmutil import get_srpm +import tempfile +import shutil +import getopt +import glob +import sys +import os + +HELP = """\ +Usage: repsys getsrpm [OPTIONS] REPPKGURL + +Options: + -c Use files in current/ directory (default) + -p Use files in pristine/ directory + -v VER Use files from the version specified by VER (e.g. 2.2.1-2cl) + -r REV Use files from current directory, in revision REV (e.g. 1001) + -t DIR Put SRPM file in directory DIR when done (default is ".") + -P USER Define the RPM packager inforamtion to USER + -s FILE Run script with "FILE TOPDIR SPECFILE" command + -n Rename the package to include the revision number + -l Use subversion log to build rpm %changelog + -h Show this message + +Examples: + repsys getsrpm http://foo.bar/svn/cnc/snapshot/python + repsys getsrpm -p http://foo.bar/svn/cnc/releases/8cl/python + repsys getsrpm -r 1001 file:///svn/cnc/snapshot/python +""" + +def mode_callback(option, opt, val, parser, mode): + opts = parser.values + opts.mode = mode + if mode == "version": + try: + opts.version, opts.release = val.split("-", 1) + except ValueError: + raise Error, "wrong version, use something like 2.2-1cl" + elif mode == "revision": + opts.revision = val + +def parse_options(): + parser = OptionParser(help=HELP) + parser.defaults["mode"] = "current" + parser.defaults["version"] = None + parser.defaults["release"] = None + parser.defaults["revision"] = None + parser.add_option("-c", action="callback", callback=mode_callback, + callback_kwargs={"mode": "current"}) + parser.add_option("-p", action="callback", callback=mode_callback, + callback_kwargs={"mode": "pristine"}) + parser.add_option("-r", action="callback", callback=mode_callback, + callback_kwargs={"mode": "revision"}) + parser.add_option("-v", action="callback", callback=mode_callback, + callback_kwargs={"mode": "version"}) + parser.add_option("-t", dest="targetdirs", action="append", default=[]) + parser.add_option("-s", dest="scripts", action="append", default=[]) + parser.add_option("-P", dest="packager", default="") + parser.add_option("-n", dest="revname", action="store_true") + parser.add_option("-l", dest="svnlog", action="store_true") + opts, args = parser.parse_args() + if len(args) != 1: + raise Error, "invalid arguments" + opts.pkgdirurl = default_parent(args[0]) + return opts + +def main(): + do_command(parse_options, get_srpm) + +# vim:et:ts=4:sw=4 diff --git a/RepSys/commands/markrelease.py b/RepSys/commands/markrelease.py new file mode 100644 index 0000000..054fff0 --- /dev/null +++ b/RepSys/commands/markrelease.py @@ -0,0 +1,96 @@ +#!/usr/bin/python +# +# This program will append a release to the Conectiva Linux package +# repository system. It's meant to be a startup system to include +# pre-packaged SRPMS in the repository, thus, you should not commit +# packages over an ongoing package structure (with changes in current/ +# directory and etc). Also, notice that packages must be included in +# cronological order. +# +from RepSys import Error +from RepSys.command import * +from RepSys.rpm import SRPM +from RepSys.rpmutil import mark_release +from RepSys.util import get_auth +import getopt +import sys +import os + +HELP = """\ +*** WARNING --- You probably SHOULD NOT use this program! --- WARNING *** + +Usage: repsys markrelease [OPTIONS] REPPKGURL + +Options: + -f FILE Try to extract information from given file + -r REV Revision which will be used to make the release copy tag + -v VER Version which will be used to make the release copy tag + -n Append package name to provided URL + -h Show this message + +Examples: + repsys markrelease -r 68 -v 1.0-1 file://svn/cnc/snapshot/foo + repsys markrelease -f @68:foo-1.0-1.src.rpm file://svn/cnc/snapshot/foo + repsys markrelease -r 68 -f foo-1.0.src.rpm file://svn/cnc/snapshot/foo +""" + +def version_callback(option, opt, val, parser): + opts = parser.values + try: + opts.version, opts.release = val.split("-", 1) + except ValueError: + raise Error, "wrong version, use something like 1:2.2-1cl" + +def parse_options(): + parser = OptionParser(help=HELP) + parser.defaults["version"] = None + parser.defaults["release"] = None + parser.add_option("-v", action="callback", callback=version_callback) + parser.add_option("-r", dest="revision") + parser.add_option("-f", dest="filename") + parser.add_option("-n", dest="appendname", action="store_true") + opts, args = parser.parse_args() + + if len(args) != 1: + raise Error, "invalid arguments" + + opts.pkgdirurl = default_parent(args[0]) + + filename = opts.filename + appendname = opts.appendname + del opts.filename, opts.appendname + + if filename: + if not os.path.isfile(filename): + raise Error, "file not found: "+filename + if not opts.revision: + basename = os.path.basename(filename) + end = basename.find(":") + if basename[0] != "@" or end == -1: + raise Error, "couldn't guess revision from filename" + opts.revision = basename[1:end] + srpm = None + if not opts.version: + srpm = SRPM(filename) + if srpm.epoch: + opts.version = "%s:%s" % (srpm.epoch, srpm.version) + else: + opts.version = srpm.version + opts.release = srpm.release + if appendname: + if not srpm: + srpm = SRPM(filename) + opts.pkgdirurl = "/".join([opts.pkgdirurl, srpm.name]) + elif appendname: + raise Error, "option -n requires option -f" + elif not opts.revision: + raise Error, "no revision provided" + elif not opts.version: + raise Error, "no version provided" + get_auth() + return opts + +def main(): + do_command(parse_options, mark_release) + +# vim:et:ts=4:sw=4 diff --git a/RepSys/commands/patchspec.py b/RepSys/commands/patchspec.py new file mode 100644 index 0000000..155ff4f --- /dev/null +++ b/RepSys/commands/patchspec.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +# +# This program will try to patch a spec file from a given package url. +# +from RepSys import Error +from RepSys.rpmutil import patch_spec +from RepSys.command import * +import getopt +import sys + +HELP = """\ +Usage: repsys patchspec [OPTIONS] REPPKGURL PATCHFILE + +Options: + -l LOG Use LOG as log message + -h Show this message + +Examples: + repsys patchspec http://repos/svn/cnc/snapshot/foo +""" + +def parse_options(): + parser = OptionParser(help=HELP) + parser.add_option("-l", dest="log", default="") + opts, args = parser.parse_args() + if len(args) != 2: + raise Error, "invalid arguments" + opts.pkgdirurl = default_parent(args[0]) + opts.patchfile = args[1] + return opts + +def main(): + do_command(parse_options, patch_spec) + +# vim:et:ts=4:sw=4 diff --git a/RepSys/commands/putsrpm.py b/RepSys/commands/putsrpm.py new file mode 100644 index 0000000..21ad234 --- /dev/null +++ b/RepSys/commands/putsrpm.py @@ -0,0 +1,61 @@ +#!/usr/bin/python +# +# This program will append a release to the Conectiva Linux package +# repository system. It's meant to be a startup system to include +# pre-packaged SRPMS in the repository, thus, you should not commit +# packages over an ongoing package structure (with changes in current/ +# directory and etc). Also, notice that packages must be included in +# cronological order. +# +from RepSys import Error +from RepSys.command import * +from RepSys.rpmutil import put_srpm +import getopt +import sys, os + +HELP = """\ +*** WARNING --- You probably SHOULD NOT use this program! --- WARNING *** + +Usage: repsys putsrpm [OPTIONS] REPPKGURL + +Options: + -n Append package name to provided URL + -l LOG Use log when commiting changes + -h Show this message + +Examples: + repsys putsrpm file://svn/cnc/snapshot/foo /cnc/d/SRPMS/foo-1.0.src.rpm +""" + +def parse_options(): + parser = OptionParser(help=HELP) + parser.add_option("-l", dest="log", default="") + parser.add_option("-n", dest="appendname", action="store_true") + opts, args = parser.parse_args() + if len(args) != 2: + raise Error, "invalid arguments" + opts.pkgdirurl = default_parent(args[0]) + opts.srpmfile = args[1] + return opts + +def put_srpm_cmd(pkgdirurl, srpmfile, appendname=0, log=""): + if os.path.isdir(srpmfile): + dir = srpmfile + for entry in os.listdir(dir): + if entry[-8:] == ".src.rpm": + sys.stderr.write("Putting %s... " % entry) + sys.stderr.flush() + entrypath = os.path.join(dir, entry) + try: + put_srpm(pkgdirurl, entrypath, appendname, log) + sys.stderr.write("done\n") + except Error, e: + sys.stderr.write("error: %s\n" % str(e)) + else: + put_srpm(pkgdirurl, srpmfile, appendname, log) + + +def main(): + do_command(parse_options, put_srpm_cmd) + +# vim:et:ts=4:sw=4 diff --git a/RepSys/commands/rpmlog.py b/RepSys/commands/rpmlog.py new file mode 100644 index 0000000..24eddfe --- /dev/null +++ b/RepSys/commands/rpmlog.py @@ -0,0 +1,40 @@ +#!/usr/bin/python +# +# This program will convert the output of "svn log" to be suitable +# for usage in an rpm %changelog session. +# +from RepSys import Error +from RepSys.command import * +from RepSys.log import svn2rpm +import getopt +import sys + +HELP = """\ +Usage: repsys rpmlog [OPTIONS] REPPKGDIRURL + +Options: + -r REV Collect logs from given revision to revision 0 + -n NUM Output only last NUM entries + -h Show this message + +Examples: + repsys rpmlog https://repos/snapshot/python +""" + +def parse_options(): + parser = OptionParser(help=HELP) + parser.add_option("-r", dest="revision") + parser.add_option("-n", dest="size", type="int") + opts, args = parser.parse_args() + if len(args) != 1: + raise Error, "invalid arguments" + opts.pkgdirurl = default_parent(args[0]) + return opts + +def rpmlog(pkgdirurl, revision, size): + sys.stdout.write(svn2rpm(pkgdirurl, revision, size)) + +def main(): + do_command(parse_options, rpmlog) + +# vim:sw=4:ts=4:et diff --git a/RepSys/commands/submit.py b/RepSys/commands/submit.py new file mode 100644 index 0000000..ff442c1 --- /dev/null +++ b/RepSys/commands/submit.py @@ -0,0 +1,104 @@ +#!/usr/bin/python +from RepSys import Error, config +from RepSys.command import * +from RepSys.rpmutil import get_spec, get_submit_info +from RepSys.util import get_auth +import urllib +import getopt +import sys +import re + +#try: +# import NINZ.client +#except ImportError: +# NINZ = None + +import xmlrpclib + +HELP = """\ +Usage: repsys submit [OPTIONS] [URL [REVISION]] + +Options: + -t TARGET Submit given package URL to given target + -l Just list available targets + -h Show this message + +Examples: + repsys submit + repsys submit foo 14800 + repsys submit https://repos/svn/cnc/snapshot/foo 14800 + repsys submit -l https://repos +""" + +def parse_options(): + parser = OptionParser(help=HELP) + parser.defaults["revision"] = "" + parser.add_option("-t", dest="target", default="Snapshot") + parser.add_option("-l", dest="list", action="store_true") + opts, args = parser.parse_args() + if not args: + name, rev = get_submit_info(".") + try: + yn = raw_input("Submit '%s', revision %d (y/N)? " % (name, rev)) + except KeyboardInterrupt: + yn = "n" + if yn.lower() in ("y", "yes"): + args = name, str(rev) + else: + print "Cancelled." + sys.exit(1) + elif len(args) > 2: + raise Error, "invalid arguments" + opts.pkgdirurl = default_parent(args[0]) + if len(args) == 2: + opts.revision = re.compile(r".*?(\d+).*").sub(r"\1", args[1]) + elif not opts.list: + raise Error, "provide -l or a revision number" + return opts + +def submit(pkgdirurl, revision, target, list=0): + #if not NINZ: + # raise Error, "you must have NINZ installed to use this command" + type, rest = urllib.splittype(pkgdirurl) + host, path = urllib.splithost(rest) + user, host = urllib.splituser(host) + host, port = urllib.splitport(host) + if type != "https": + raise Error, "you must use https:// urls" + if user: + user, passwd = urllib.splitpasswd(user) + if passwd: + raise Error, "do not use a password in your command line" + user, passwd = get_auth(username=user) + #soap = NINZ.client.Binding(host=host, + # url="https://%s/scripts/cnc/soap" % host, + # ssl=1, + # auth=(NINZ.client.AUTH.httpbasic, + # user, passwd)) + if port: + port = ":"+port + else: + port = "" + iface = xmlrpclib.ServerProxy("https://%s:%s@%s%s/scripts/cnc/xmlrpc" + % (user, passwd, host, port)) + try: + if list: + targets = iface.submit_targets() + if not targets: + raise Error, "no targets available" + sys.stdout.writelines(['"%s"\n' % x for x in targets]) + else: + iface.submit_package(pkgdirurl, revision, target) + print "Package submitted!" + #except NINZ.client.SoapError, e: + except xmlrpclib.ProtocolError, e: + raise Error, "remote error: "+str(e.errmsg) + except xmlrpclib.Fault, e: + raise Error, "remote error: "+str(e.faultString) + except xmlrpclib.Error, e: + raise Error, "remote error: "+str(e) + +def main(): + do_command(parse_options, submit) + +# vim:et:ts=4:sw=4 diff --git a/RepSys/log.py b/RepSys/log.py new file mode 100644 index 0000000..fa007fa --- /dev/null +++ b/RepSys/log.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +from RepSys import Error, config +from RepSys.svn import SVN +import tempfile +import shutil +import time +import os + +def svn2rpm(pkgdirurl, rev=None, size=None): + concat = config.get("log", "concat", "").split() + svn = SVN() + log = svn.log(os.path.join(pkgdirurl, "current"), start=rev) + if (size is not None): + log = log[:size] + rpmlog = [] + lastauthor = None + for logentry in log: + entryheader = [] + if lastauthor != logentry.author or \ + not (logentry.author in concat or "*" in concat): + entryheader.append(time.strftime("* %a %b %d %Y ", logentry.date)) + entryheader.append(config.get("users", logentry.author, + logentry.author)) + entryheader.append("\n") + entryheader.append(time.strftime("+ %Y-%m-%d %H:%M:%S", + logentry.date)) + entryheader.append(" (%d)" % logentry.revision) + if lastauthor: + rpmlog.append("") + lastauthor = logentry.author + entrylines = [] + first = 1 + for line in logentry.lines: + if line: + line = line.replace("%", "%%") + if first: + first = 0 + if entryheader: + rpmlog.append("".join(entryheader)) + line = line.lstrip() + if line[0] != "-": + nextline = "- " + line + else: + nextline = line + elif line[0] != " " and line[0] != "-": + nextline = " " + line + else: + nextline = line + if nextline not in entrylines: + rpmlog.append(nextline) + entrylines.append(nextline) + return "\n".join(rpmlog)+"\n" + +def specfile_svn2rpm(pkgdirurl, specfile, rev=None, size=None): + file = open(specfile) + lines = file.readlines() + file.close() + newlines = [] + found = 0 + + # Strip old changelogs + for line in lines: + if line.startswith("%changelog"): + found = 1 + elif not found: + newlines.append(line) + elif line.startswith("%"): + found = 0 + newlines.append(line) + + # Create new changelog + newlines.append("\n\n%changelog\n") + newlines.append(svn2rpm(pkgdirurl, rev, size)) + + # Merge old changelog, if available + oldurl = config.get("log", "oldurl") + if oldurl: + svn = SVN() + tmpdir = tempfile.mktemp() + try: + pkgname = os.path.basename(pkgdirurl) + pkgoldurl = os.path.join(oldurl, pkgname) + if svn.ls(pkgoldurl, noerror=1): + svn.checkout(pkgoldurl, tmpdir, rev=rev) + logfile = os.path.join(tmpdir, "log") + if os.path.isfile(logfile): + file = open(logfile) + newlines.append("\n") + newlines.append(file.read()) + file.close() + finally: + if os.path.isdir(tmpdir): + shutil.rmtree(tmpdir) + + # Write new specfile + file = open(specfile, "w") + file.writelines(newlines) + file.close() + +# vim:et:ts=4:sw=4 diff --git a/RepSys/pexpect.py b/RepSys/pexpect.py new file mode 100644 index 0000000..2460052 --- /dev/null +++ b/RepSys/pexpect.py @@ -0,0 +1,1050 @@ +""" +Pexpect is a Python module for spawning child applications; +controlling them; and responding to expected patterns in their output. +Pexpect can be used for automating interactive applications such as +ssh, ftp, passwd, telnet, etc. It can be used to a automate setup scripts +for duplicating software package installations on different servers. +It can be used for automated software testing. Pexpect is in the spirit of +Don Libes' Expect, but Pexpect is pure Python. Other Expect-like +modules for Python require TCL and Expect or require C extensions to +be compiled. Pexpect does not use C, Expect, or TCL extensions. It +should work on any platform that supports the standard Python pty +module. The Pexpect interface focuses on ease of use so that simple +tasks are easy. + +Pexpect is Open Source, Free, and all Good that stuff. +License: Python Software Foundation License + http://www.opensource.org/licenses/PythonSoftFoundation.html + +Noah Spurrier + +$Revision$ +$Date$ +""" + + +try: + import os, sys + import select + import string + import re + import struct + import resource + from types import * + import pty + import tty + import termios + import fcntl +except ImportError, e: + raise ImportError, str(e) + """ +A critical module was not found. Probably this OS does not support it. +Currently pexpect is intended for UNIX operating systems.""" + + + +__version__ = '0.99' +__revision__ = '$Revision$' +__all__ = ['ExceptionPexpect', 'EOF', 'TIMEOUT', 'spawn', 'run', + '__version__', '__revision__'] + + + +# Exception classes used by this module. +class ExceptionPexpect(Exception): + """Base class for all exceptions raised by this module.""" + def __init__(self, value): + self.value = value + def __str__(self): + return `self.value` +class EOF(ExceptionPexpect): + """Raised when EOF is read from a child.""" +class TIMEOUT(ExceptionPexpect): + """Raised when a read time exceeds the timeout.""" +##class MAXBUFFER(ExceptionPexpect): +## """Raised when a scan buffer fills before matching an expected pattern.""" + + + +def run (command, args=[], timeout=30): + """This runs a command; waits for it to finish; then returns + all output as a string. This is a utility interface around + the spawn class. + """ + child = spawn(command, args, timeout) + child.expect (EOF) + return child.before + +class spawn: + """This is the main class interface for Pexpect. Use this class to + start and control child applications. + """ + + def __init__(self, command, args=[], timeout=30): + """This is the constructor. The command parameter may be a string + that includes a command and any arguments to the command. For example: + p = pexpect.spawn ('/usr/bin/ftp') + p = pexpect.spawn ('/usr/bin/ssh user@example.com') + p = pexpect.spawn ('ls -latr /tmp') + You may also construct it with a list of arguments like so: + p = pexpect.spawn ('/usr/bin/ftp', []) + p = pexpect.spawn ('/usr/bin/ssh', ['user@example.com']) + p = pexpect.spawn ('ls', ['-latr', '/tmp']) + After this the child application will be created and + will be ready to talk to. For normal use, see expect() and + send() and sendline(). + + If the command parameter is an integer AND a valid file descriptor + then spawn will talk to the file descriptor instead. This can be + used to act expect features to any file descriptor. For example: + fd = os.open ('somefile.txt', os.O_RDONLY) + s = pexpect.spawn (fd) + The original creator of the file descriptor is responsible + for closing it. Spawn will not try to close it and spawn will + raise an exception if you try to call spawn.close(). + """ + + self.STDIN_FILENO = sys.stdin.fileno() + self.STDOUT_FILENO = sys.stdout.fileno() + self.STDERR_FILENO = sys.stderr.fileno() + + self.stdin = sys.stdin + self.stdout = sys.stdout + self.stderr = sys.stderr + + self.timeout = timeout + self.child_fd = -1 # initially closed + self.__child_fd_owner = None + self.exitstatus = None + self.pid = None + self.log_file = None + self.before = None + self.after = None + self.match = None + self.softspace = 0 # File-like object. + self.name = '' # File-like object. + self.flag_eof = 0 + + # NEW -- to support buffering -- the ability to read more than one + # byte from a TTY at a time. See setmaxread() method. + self.buffer = '' + self.maxread = 1 # Maximum to read at a time + ### IMPLEMENT THIS FEATURE!!! + # anything before maxsearchsize point is preserved, but not searched. + #self.maxsearchsize = 1000 + + # If command is an int type then it must represent an open file descriptor. + if type (command) == type(0): + try: # Command is an int, so now check if it is a file descriptor. + os.fstat(command) + except OSError: + raise ExceptionPexpect, 'Command is an int type, yet is not a valid file descriptor.' + self.pid = -1 + self.child_fd = command + self.__child_fd_owner = 0 # Sets who is reponsible for the child_fd + self.args = None + self.command = None + self.name = '<file descriptor>' + return + + if type (args) != type([]): + raise TypeError, 'The second argument, args, must be a list.' + + if args == []: + self.args = _split_command_line(command) + self.command = self.args[0] + else: + self.args = args + self.args.insert (0, command) + self.command = command + self.name = '<' + reduce(lambda x, y: x+' '+y, self.args) + '>' + + self.__spawn() + + def __del__(self): + """This makes sure that no system resources are left open. + Python only garbage collects Python objects. OS file descriptors + are not Python objects, so they must be handled explicitly. + If the child file descriptor was opened outside of this class + (passed to the constructor) then this does not close it. + """ + try: # CNC:2003-09-23 + if self.__child_fd_owner: + self.close() + except OSError: + pass + + def __spawn(self): + """This starts the given command in a child process. This does + all the fork/exec type of stuff for a pty. This is called by + __init__. The args parameter is a list, command is a string. + """ + # The pid and child_fd of this object get set by this method. + # Note that it is difficult for this method to fail. + # You cannot detect if the child process cannot start. + # So the only way you can tell if the child process started + # or not is to try to read from the file descriptor. If you get + # EOF immediately then it means that the child is already dead. + # That may not necessarily be bad, because you may haved spawned a child + # that performs some task; creates no stdout output; and then dies. + # It is a fuzzy edge case. Any child process that you are likely to + # want to interact with Pexpect would probably not fall into this + # category. + # FYI, This essentially does a fork/exec operation. + + assert self.pid == None, 'The pid member is not None.' + assert self.command != None, 'The command member is None.' + + if _which(self.command) == None: + raise ExceptionPexpect ('The command was not found or was not executable: %s.' % self.command) + + try: + self.pid, self.child_fd = pty.fork() + except OSError, e: + raise ExceptionPexpect('Pexpect: pty.fork() failed: ' + str(e)) + + if self.pid == 0: # Child + try: # Some platforms do not like setwinsize (Cygwin). + self.child_fd = sys.stdout.fileno() + self.setwinsize(24, 80) + except: + pass + # Do not allow child to inherit open file descriptors from parent. + max_fd = resource.getrlimit(resource.RLIMIT_NOFILE)[0] + for i in range (3, max_fd): + try: + os.close (i) + except OSError: + pass + + os.environ["LANG"] = "C" + os.execvp(self.command, self.args) + + # Parent + self.__child_fd_owner = 1 + + def fileno (self): # File-like object. + """This returns the file descriptor of the pty for the child.""" + return self.child_fd + + def close (self, wait=1): # File-like object. + """ This closes the connection with the child application. + It makes no attempt to actually kill the child or wait for its status. + If the file descriptor was set by passing a file descriptor + to the constructor then this method raises an exception. + Note that calling close() more than once is valid. + This emulates standard Python behavior with files. + If wait is set to True then close will wait + for the exit status of the process. Doing a wait is a blocking call, + but this usually takes almost no time at all. Generally, + you don't have to worry about this. If you are + creating lots of children then you usually want to call wait. + Only set wait to false if you know the child will + continue to run after closing the controlling TTY. + Otherwise you will end up with defunct (zombie) processes. + """ + if self.child_fd != -1: + if not self.__child_fd_owner: + raise ExceptionPexpect ('This file descriptor cannot be closed because it was not created by spawn. The original creator is responsible for closing it.') + self.flush() + os.close (self.child_fd) + if wait: + os.waitpid (self.pid, 0) + self.child_fd = -1 + self.__child_fd_owner = None + + def flush (self): # File-like object. + """This does nothing. It is here to support the interface for a File-like object. + """ + pass + + def isatty (self): # File-like object. + """This returns 1 if the file descriptor is open and + connected to a tty(-like) device, else 0. + """ + return os.isatty(self.child_fd) + + def setecho (self, on): + """This sets the terminal echo mode on or off.""" + new = termios.tcgetattr(self.child_fd) + if on: + new[3] = new[3] | termios.ECHO # lflags + else: + new[3] = new[3] & ~termios.ECHO # lflags + termios.tcsetattr(self.child_fd, termios.TCSADRAIN, new) + + def setlog (self, fileobject): + """This sets logging output to go to the given fileobject. + Set fileobject to None to stop logging. + Example: + child = pexpect.spawn('some_command') + fout = file('mylog.txt','w') + child.setlog (fout) + ... + """ + self.log_file = fileobject + + def setmaxread (self, maxread): + """This sets the maximum number of bytes to read from a TTY at one time. + This is used to change the read buffer size. When a pexpect.spawn + object is created the default maxread is 1 (unbuffered). + Set this value higher to turn on buffer. This should help performance + in cases where large amounts of output are read back from the child. + """ + self.maxread = maxread + + def read_nonblocking (self, size = 1, timeout = None): + """ + This reads at most size characters from the child application. + It includes a timeout. If the read does not complete within the + timeout period then a TIMEOUT exception is raised. + If the end of file is read then an EOF exception will be raised. + If a log file was set using setlog() then all data will + also be written to the log file. + + Notice that if this method is called with timeout=None + then it actually may block. + + This is a non-blocking wrapper around os.read(). + It uses select.select() to implement a timeout. + """ + + if self.child_fd == -1: + raise ValueError ('I/O operation on closed file') + + # Note that some systems like Solaris don't seem to ever give + # an EOF when the child dies. In fact, you can still try to read + # from the child_fd -- it will block forever or until TIMEOUT. + # For this case, I test isalive() before doing any reading. + # If isalive() is false, then I pretend that this is the same as EOF. + if not self.isalive(): + r, w, e = select.select([self.child_fd], [], [], 0) + if not r: + self.flag_eof = 1 + raise EOF ('End Of File (EOF) in read(). Braindead platform.') + + r, w, e = select.select([self.child_fd], [], [], timeout) + if not r: + raise TIMEOUT('Timeout exceeded in read().') + + if self.child_fd in r: + try: + s = os.read(self.child_fd, size) + except OSError, e: + self.flag_eof = 1 + raise EOF('End Of File (EOF) in read(). Exception style platform.') + if s == '': + self.flag_eof = 1 + raise EOF('End Of File (EOF) in read(). Empty string style platform.') + + if self.log_file != None: + self.log_file.write (s) + self.log_file.flush() + + return s + + raise ExceptionPexpect('Reached an unexpected state in read().') + + def read (self, size = -1): # File-like object. + """This reads at most size bytes from the file + (less if the read hits EOF before obtaining size bytes). + If the size argument is negative or omitted, + read all data until EOF is reached. + The bytes are returned as a string object. + An empty string is returned when EOF is encountered immediately. + """ + if size == 0: + return '' + if size < 0: + self.expect (EOF) + return self.before + + # I could have done this more directly by not using expect(), but + # I deliberately decided to couple read() to expect() so that + # I would catch any bugs early and ensure consistant behavior. + # It's a little less efficient, but there is less for me to + # worry about if I have to later modify read() or expect(). + cre = re.compile('.{%d}' % size, re.DOTALL) + index = self.expect ([cre, EOF]) + if index == 0: + return self.after ### self.before should be ''. Should I assert this? + return self.before + + def readline (self, size = -1): # File-like object. + """This reads and returns one entire line. A trailing newline is kept in + the string, but may be absent when a file ends with an incomplete line. + Note: This readline() looks for a \r\n pair even on UNIX because this is + what the pseudo tty device returns. So contrary to what you may be used to + you will get a newline as \r\n. + An empty string is returned when EOF is hit immediately. + Currently, the size agument is mostly ignored, so this behavior is not + standard for a file-like object. + """ + if size == 0: + return '' + index = self.expect (['\r\n', EOF]) + if index == 0: + return self.before + '\r\n' + else: + return self.before + + def __iter__ (self): + """This is to support interators over a file-like object. + """ + return self + def next (self): + """This is to support iterators over a file-like object. + """ + result = self.readline() + if result == "": + raise StopIteration + return result + + def readlines (self, sizehint = -1): # File-like object. + """This reads until EOF using readline() and returns a list containing + the lines thus read. The optional sizehint argument is ignored. + """ + lines = [] + while 1: + line = self.readline() + if not line: + break + lines.append(line) + return lines + + def write(self, str): # File-like object. + """This is similar to send() except that there is no return value. + """ + self.send (str) + + def writelines (self, sequence): # File-like object. + """This calls write() for each element in the sequence. + The sequence can be any iterable object producing strings, + typically a list of strings. This does not add line separators + There is no return value. + """ + for str in sequence: + self.write (str) + + def send(self, str): + """This sends a string to the child process. + This returns the number of bytes written. + """ + return os.write(self.child_fd, str) + + def sendline(self, str=''): + """This is like send(), but it adds a line feed (os.linesep). + This returns the number of bytes written. + """ + n = self.send(str) + n = n + self.send (os.linesep) + return n + + def sendeof(self): + """This sends an EOF to the child. + This sends a character which causes the pending parent output + buffer to be sent to the waiting child program without + waiting for end-of-line. If it is the first character of the + line, the read() in the user program returns 0, which + signifies end-of-file. This means to work as expected + a sendeof() has to be called at the begining of a line. + This method does not send a newline. It is the responsibility + of the caller to ensure the eof is sent at the beginning of a line. + """ + ### Hmmm... how do I send an EOF? + ###C if ((m = write(pty, *buf, p - *buf)) < 0) + ###C return (errno == EWOULDBLOCK) ? n : -1; + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) # remember current state + new = termios.tcgetattr(fd) + new[3] = new[3] | termios.ICANON # lflags + # use try/finally to ensure state gets restored + try: + # EOF is recognized when ICANON is set, so make sure it is set. + termios.tcsetattr(fd, termios.TCSADRAIN, new) + os.write (self.child_fd, '%c' % termios.CEOF) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old) # restore state + + def eof (self): + """This returns 1 if the EOF exception was raised at some point. + """ + return self.flag_eof + + def isalive(self): + """This tests if the child process is running or not. + This returns 1 if the child process appears to be running or 0 if not. + This also sets the exitstatus attribute. + It can take literally SECONDS for Solaris to return the right status. + This is the most wiggly part of Pexpect, but I think I've almost got + it nailed down. + """ + # I can't use signals. Signals on UNIX suck and they + # mess up Python pipes (setting SIGCHLD to SIGIGNORE). + + # If this class was created from an existing file descriptor then + # I just check to see if the file descriptor is still valid. + if self.pid == -1 and not self.__child_fd_owner: + try: + os.fstat(self.child_fd) + return 1 + except: + return 0 + + try: + pid, status = os.waitpid(self.pid, os.WNOHANG) + except OSError: + return 0 + + # I have to do this twice for Solaris. + # I can't even believe that I figured this out... + if pid == 0 and status == 0: + try: + pid, status = os.waitpid(self.pid, os.WNOHANG) + #print 'Solaris sucks' + except OSError: # This is crufty. When does this happen? + return 0 + # If pid and status is still 0 after two calls to waitpid() then + # the process really is alive. This seems to work on all platforms. + if pid == 0 and status == 0: + return 1 + + # I do not OR this together because I want hooks for debugging. + if os.WIFEXITED (status): + self.exitstatus = os.WEXITSTATUS(status) + return 0 + elif os.WIFSTOPPED (status): + return 0 + elif os.WIFSIGNALED (status): + return 0 + else: + return 0 # Can I ever get here? + + def kill(self, sig): + """This sends the given signal to the child application. + In keeping with UNIX tradition it has a misleading name. + It does not necessarily kill the child unless + you send the right signal. + """ + # Same as os.kill, but the pid is given for you. + if self.isalive(): + os.kill(self.pid, sig) + + def compile_pattern_list(self, patterns): + """This compiles a pattern-string or a list of pattern-strings. + Patterns must be a StringType, EOF, TIMEOUT, SRE_Pattern, or + a list of those. + + This is used by expect() when calling expect_list(). + Thus expect() is nothing more than:: + cpl = self.compile_pattern_list(pl) + return self.expect_list(clp, timeout) + + If you are using expect() within a loop it may be more + efficient to compile the patterns first and then call expect_list(). + This avoid calls in a loop to compile_pattern_list(): + cpl = self.compile_pattern_list(my_pattern) + while some_condition: + ... + i = self.expect_list(clp, timeout) + ... + """ + if type(patterns) is not ListType: + patterns = [patterns] + + compiled_pattern_list = [] + for p in patterns: + if type(p) is StringType: + compiled_pattern_list.append(re.compile(p, re.DOTALL)) + elif p is EOF: + compiled_pattern_list.append(EOF) + elif p is TIMEOUT: + compiled_pattern_list.append(TIMEOUT) + elif type(p) is type(re.compile('')): + compiled_pattern_list.append(p) + else: + raise TypeError, 'Argument must be one of StringType, EOF, TIMEOUT, SRE_Pattern, or a list of those type. %s' % str(type(p)) + + return compiled_pattern_list + + def expect(self, pattern, timeout = -1): + """This seeks through the stream until a pattern is matched. + The pattern is overloaded and may take several types including a list. + The pattern can be a StringType, EOF, a compiled re, or + a list of those types. Strings will be compiled to re types. + This returns the index into the pattern list. If the pattern was + not a list this returns index 0 on a successful match. + This may raise exceptions for EOF or TIMEOUT. + To avoid the EOF or TIMEOUT exceptions add EOF or TIMEOUT to + the pattern list. + + After a match is found the instance attributes + 'before', 'after' and 'match' will be set. + You can see all the data read before the match in 'before'. + You can see the data that was matched in 'after'. + The re.MatchObject used in the re match will be in 'match'. + If an error occured then 'before' will be set to all the + data read so far and 'after' and 'match' will be None. + + If timeout is -1 then timeout will be set to the self.timeout value. + + Note: A list entry may be EOF or TIMEOUT instead of a string. + This will catch these exceptions and return the index + of the list entry instead of raising the exception. + The attribute 'after' will be set to the exception type. + The attribute 'match' will be None. + This allows you to write code like this: + index = p.expect (['good', 'bad', pexpect.EOF, pexpect.TIMEOUT]) + if index == 0: + do_something() + elif index == 1: + do_something_else() + elif index == 2: + do_some_other_thing() + elif index == 3: + do_something_completely_different() + instead of code like this: + try: + index = p.expect (['good', 'bad']) + if index == 0: + do_something() + elif index == 1: + do_something_else() + except EOF: + do_some_other_thing() + except TIMEOUT: + do_something_completely_different() + These two forms are equivalent. It all depends on what you want. + You can also just expect the EOF if you are waiting for all output + of a child to finish. For example: + p = pexpect.spawn('/bin/ls') + p.expect (pexpect.EOF) + print p.before + """ + compiled_pattern_list = self.compile_pattern_list(pattern) + return self.expect_list(compiled_pattern_list, timeout) + + def expect_exact (self, pattern_list, timeout = -1): + """This is similar to expect() except that it takes + list of plain strings instead of regular expressions. + The idea is that this should be much faster. It could also be + useful when you don't want to have to worry about escaping + regular expression characters that you want to match. + You may also pass just a string without a list and the single + string will be converted to a list. + If timeout is -1 then timeout will be set to the self.timeout value. + """ + ### This is dumb. It shares most of the code with expect_list. + ### The only different is the comparison method and that + ### self.match is always None after calling this. + if timeout == -1: + timeout = self.timeout + + if type(pattern_list) is StringType: + pattern_list = [pattern_list] + + try: + #ED# incoming = '' + incoming = self.buffer + while 1: # Keep reading until exception or return. + #ED# c = self.read_nonblocking (1, timeout) + #ED# incoming = incoming + c + + # Sequence through the list of patterns and look for a match. + index = -1 + for str_target in pattern_list: + index = index + 1 + if str_target is EOF or str_target is TIMEOUT: + continue # The Exception patterns are handled differently. + match_index = incoming.find (str_target) + if match_index >= 0: + self.before = incoming [ : match_index] + self.after = incoming [match_index : ] + self.buffer = incoming [match_index + len(str_target):] + self.match = None + return index + c = self.read_nonblocking (self.maxread, timeout) + incoming = incoming + c + + except EOF: + if EOF in pattern_list: + self.before = incoming + self.after = EOF + self.buffer = '' + return pattern_list.index(EOF) + else: + raise + except TIMEOUT: + if TIMEOUT in pattern_list: + self.before = incoming + self.after = TIMEOUT + self.buffer = '' + return pattern_list.index(TIMEOUT) + else: + raise + except Exception: + self.before = incoming + self.after = None + self.match = None + self.buffer = '' + raise + + def expect_list(self, pattern_list, timeout = -1): + """ + This takes a list of compiled regular expressions and returns + the index into the pattern_list that matched the child's output. + This is called by expect(). It is similar to the expect() method + except that expect_list() is not overloaded. You must not pass + anything except a list of compiled regular expressions. + If timeout is -1 then timeout will be set to the self.timeout value. + """ + + if timeout == -1: + timeout = self.timeout + + try: + #ED# incoming = '' + incoming = self.buffer + while 1: # Keep reading until exception or return. + #ED# c = self.read_nonblocking (1, timeout) + #ED# incoming = incoming + c + + # Sequence through the list of patterns and look for a match. + index = -1 + for cre in pattern_list: + index = index + 1 + if cre is EOF or cre is TIMEOUT: + continue # The patterns for PexpectExceptions are handled differently. + match = cre.search(incoming) + if match is not None: + self.before = incoming[ : match.start()] + self.after = incoming[match.start() : ] + self.match = match + self.buffer = incoming[match.end() : ] + return index + # Read more data + c = self.read_nonblocking (self.maxread, timeout) + incoming = incoming + c + + except EOF: + if EOF in pattern_list: + self.before = incoming + self.after = EOF + self.buffer = '' + return pattern_list.index(EOF) + else: + raise + except TIMEOUT: + if TIMEOUT in pattern_list: + self.before = incoming + self.after = TIMEOUT + self.buffer = '' + return pattern_list.index(TIMEOUT) + else: + raise + except Exception: + self.before = incoming + self.after = None + self.match = None + self.buffer = '' + raise + + def getwinsize(self): + """ + This returns the window size of the child tty. + The return value is a tuple of (rows, cols). + """ + + s = struct.pack('HHHH', 0, 0, 0, 0) + x = fcntl.ioctl(self.fileno(), termios.TIOCGWINSZ, s) + return struct.unpack('HHHH', x)[0:2] + + def setwinsize(self, r, c): + """ + This sets the windowsize of the child tty. + This will cause a SIGWINCH signal to be sent to the child. + This does not change the physical window size. + It changes the size reported to TTY-aware applications like + vi or curses -- applications that respond to the SIGWINCH signal. + """ + # Check for buggy platforms. Some Python versions on some platforms + # (notably OSF1 Alpha and RedHat 7.1) truncate the value for + # termios.TIOCSWINSZ. It is not clear why this happens. + # These platforms don't seem to handle the signed int very well; + # yet other platforms like OpenBSD have a large negative value for + # TIOCSWINSZ and they don't have a truncate problem. + # Newer versions of Linux have totally different values for TIOCSWINSZ. + # + # Note that this fix is a hack. + TIOCSWINSZ = termios.TIOCSWINSZ + if TIOCSWINSZ == 2148037735L: # L is not required in Python >= 2.2. + TIOCSWINSZ = -2146929561 # Same bits, but with sign. + + # Note, assume ws_xpixel and ws_ypixel are zero. + s = struct.pack('HHHH', r, c, 0, 0) + fcntl.ioctl(self.fileno(), TIOCSWINSZ, s) + + def interact(self, escape_character = chr(29)): + """This gives control of the child process to the interactive user + (the human at the keyboard). + Keystrokes are sent to the child process, and the stdout and stderr + output of the child process is printed. + When the user types the escape_character this method will stop. + The default for escape_character is ^] (ASCII 29). + This simply echos the child stdout and child stderr to the real + stdout and it echos the real stdin to the child stdin. + """ + # Flush the buffer. + self.stdout.write (self.buffer) + self.buffer = '' + self.stdout.flush() + mode = tty.tcgetattr(self.STDIN_FILENO) + tty.setraw(self.STDIN_FILENO) + try: + self.__interact_copy(escape_character) + finally: + tty.tcsetattr(self.STDIN_FILENO, tty.TCSAFLUSH, mode) + + def __interact_writen(self, fd, data): + """This is used by the interact() method. + """ + while data != '' and self.isalive(): + n = os.write(fd, data) + data = data[n:] + def __interact_read(self, fd): + """This is used by the interact() method. + """ + return os.read(fd, 1000) + def __interact_copy(self, escape_character = None): + """This is used by the interact() method. + """ + while self.isalive(): + r, w, e = select.select([self.child_fd, self.STDIN_FILENO], [], []) + if self.child_fd in r: + data = self.__interact_read(self.child_fd) + os.write(self.STDOUT_FILENO, data) + if self.STDIN_FILENO in r: + data = self.__interact_read(self.STDIN_FILENO) + self.__interact_writen(self.child_fd, data) + if escape_character in data: + break + +############################################################################## +# End of Spawn +############################################################################## + +def _which (filename): + """This takes a given filename; tries to find it in the + environment path; then checks if it is executable. + """ + + # Special case where filename already contains a path. + if os.path.dirname(filename) != '': + if os.access (filename, os.X_OK): + return filename + + if not os.environ.has_key('PATH') or os.environ['PATH'] == '': + p = os.defpath + else: + p = os.environ['PATH'] + + # Oddly enough this was the one line that made Pexpect + # incompatible with Python 1.5.2. + #pathlist = p.split (os.pathsep) + pathlist = string.split (p, os.pathsep) + + for path in pathlist: + f = os.path.join(path, filename) + if os.access(f, os.X_OK): + return f + return None + +def _split_command_line(command_line): + """This splits a command line into a list of arguments. + It splits arguments on spaces, but handles + embedded quotes, doublequotes, and escaped characters. + It's impossible to do this with a regular expression, so + I wrote a little state machine to parse the command line. + """ + arg_list = [] + arg = '' + state_quote = 0 + state_doublequote = 0 + state_esc = 0 + for c in command_line: + if c == '\\': # Escape the next character + state_esc = 1 + elif c == r"'": # Handle single quote + if state_esc: + state_esc = 0 + elif not state_quote: + state_quote = 1 + else: + state_quote = 0 + elif c == r'"': # Handle double quote + if state_esc: + state_esc = 0 + elif not state_doublequote: + state_doublequote = 1 + else: + state_doublequote = 0 + + # Add arg to arg_list unless in some other state. + elif c == ' 'and not state_quote and not state_doublequote and not state_esc: + arg_list.append(arg) + arg = '' + else: + arg = arg + c + if c != '\\'and state_esc: # escape mode lasts for one character. + state_esc = 0 + + # Handle last argument. + if arg != '': + arg_list.append(arg) + return arg_list + +import shlex +def _split_command_line(command_line): + return shlex.split(command_line) + + +#################### +# +# NOTES +# +#################### + +## def send_human(self, text, delay_min = 0, delay_max = 1): +## pass +## def spawn2(self, command, args): +## """return pid, fd_stdio, fd_stderr +## """ +## pass + + +# Reason for double fork: +# http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC15 +# Reason for ptys: +# http://www.erlenstar.demon.co.uk/unix/faq_4.html#SEC52 + +# Nonblocking on Win32? +# Reasearch this as a way to maybe make pipe work for Win32. +# http://groups.google.com/groups?q=setraw+tty&hl=en&selm=uvgpvisvk.fsf%40roundpoint.com&rnum=7 +# +# if istty: +# if os.name=='posix': +# import tty +# tty.setraw(sys.stdin.fileno()) +# elif os.name=='nt': +# import win32file, win32con +# hstdin = win32file._get_osfhandle(sys.stdin.fileno()) +# modes = (win32file.GetConsoleMode(hstdin) +# & ~(win32con.ENABLE_LINE_INPUT +# |win32con.ENABLE_ECHO_INPUT)) +# win32file.SetConsoleMode(hstdin, modes) + +# Basic documentation: +# Explain use of lists of patterns and return index. +# Explain exceptions for non-handled special cases like EOF + +# Test bad fork +# Test ENOENT. In other words, no more TTY devices. + +#GLOBAL_SIGCHLD_RECEIVED = 0 +#def childdied (signum, frame): +# print 'Signal handler called with signal', signum +# frame.f_globals['pexpect'].GLOBAL_SIGCHLD_RECEIVED = 1 +# print str(frame.f_globals['pexpect'].GLOBAL_SIGCHLD_RECEIVED) +# GLOBAL_SIGCHLD_RECEIVED = 1 + +### Weird bug. If you read too fast after doing a sendline() +# Sometimes you will read the data back that you just sent even if +# the child did not echo the data. This is particularly a problem if +# you send a password. + +# This was previously used to implement a look-ahead in reads. +# if the lookahead failed then Pexpect would "push-back" the data +# that was read. The idea was to allow read() to read blocks of data. +# What I do now is just read one character at a time and then try a +# match. This is not as efficient, but it works well enough for the +# output of most applications and it makes program logic much simpler. +##class PushbackReader: +## """This class is a wrapper around os.read. It adds the features of buffering +## to allow push-back of data and to provide a timeout on a read. +## """ +## def __init__(self, file_descriptor): +## self.fd = file_descriptor +## self.buffer = '' +## +## def read(self, n, timeout = None): +## """This does a read restricted by a timeout and +## it includes any cached data from previous calls. +## This is a non-blocking wrapper around os.read. +## it uses select.select to supply a timeout. +## Note that if this is called with timeout=None (the default) +## then this actually MAY block. +## """ +## # The read() call is a problem. +## # Some platforms return an empty string '' at EOF. +## # Whereas other platforms raise an Input/output exception. +## +## avail = len(self.buffer) +## if n > avail: +## result = self.buffer +## n = n-avail +## else: +## result = self.buffer[: n] +## self.buffer = self.buffer[n:] +## +## r, w, e = select.select([self.fd], [], [], timeout) +## if not r: +## self.flag_timeout = 1 +## raise TIMEOUT('Read exceeded time: %d'%timeout) +## +## if self.fd in r: +## try: +## s = os.read(self.fd, n) +## except OSError, e: +## self.flag_eof = 1 +## raise EOF('Read reached End Of File (EOF). Exception platform.') +## if s == '': +## self.flag_eof = 1 +## raise EOF('Read reached End Of File (EOF). Empty string platform.') +## return s +## +## self.flag_error = 1 +## raise ExceptionPexpect('PushbackReader.read() reached an unexpected state.'+ +## ' There is a logic error in the Pexpect source code.') +## +## def pushback(self, data): +## self.buffer = piece+self.buffer + + +#def _setwinsize(r, c): +# """This sets the windowsize of the tty for stdout. +# This does not change the physical window size. +# It changes the size reported to TTY-aware applications like +# vi or curses -- applications that respond to the SIGWINCH signal. +# This is used by __spawn to set the tty window size of the child. +# """ +# # Check for buggy platforms. Some Pythons on some platforms +# # (notably OSF1 Alpha and RedHat 7.1) truncate the value for +# # termios.TIOCSWINSZ. It is not clear why this happens. +# # These platforms don't seem to handle the signed int very well; +# # yet other platforms like OpenBSD have a large negative value for +# # TIOCSWINSZ and they don't truncate. +# # Newer versions of Linux have totally different values for TIOCSWINSZ. +# # +# # Note that this fix is a hack. +# TIOCSWINSZ = termios.TIOCSWINSZ +# if TIOCSWINSZ == 2148037735L: # L is not required in Python 2.2. +# TIOCSWINSZ = -2146929561 # Same number in binary, but with sign. +# +# # Assume ws_xpixel and ws_ypixel are zero. +# s = struct.pack("HHHH", r, c, 0, 0) +# fcntl.ioctl(sys.stdout.fileno(), TIOCSWINSZ, s) +# +#def _getwinsize(): +# s = struct.pack("HHHH", 0, 0, 0, 0) +# x = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, s) +# return struct.unpack("HHHH", x) + diff --git a/RepSys/rpm.py b/RepSys/rpm.py new file mode 100644 index 0000000..d448c5f --- /dev/null +++ b/RepSys/rpm.py @@ -0,0 +1,19 @@ +#!/usr/bin/python +from RepSys.util import execcmd + +class SRPM: + def __init__(self, filename): + self.filename = filename + self._getinfo() + + def _getinfo(self): + cmdstr = "rpm -qp --qf '%%{name} %%{epoch} %%{release} %%{version}' %s" + status, output = execcmd(cmdstr % self.filename) + self.name, self.epoch, self.release, self.version = output.split() + if self.epoch == "(none)": + self.epoch = None + + def unpack(self, topdir): + execcmd("rpm -i --define '_topdir %s' %s" % (topdir, self.filename)) + +# vim:et:ts=4:sw=4 diff --git a/RepSys/rpmutil.py b/RepSys/rpmutil.py new file mode 100644 index 0000000..3df9818 --- /dev/null +++ b/RepSys/rpmutil.py @@ -0,0 +1,369 @@ +#!/usr/bin/python +from RepSys import Error, config +from RepSys.svn import SVN +from RepSys.rpm import SRPM +from RepSys.log import specfile_svn2rpm +from RepSys.util import execcmd +import tempfile +import shutil +import glob +import sys +import os + +def get_spec(pkgdirurl, targetdir="."): + svn = SVN() + tmpdir = tempfile.mktemp() + try: + geturl = "/".join([pkgdirurl, "current", "SPECS"]) + svn.checkout("'%s'" % geturl, tmpdir) + speclist = glob.glob(os.path.join(tmpdir, "*.spec")) + if not speclist: + raise Error, "no spec files found" + spec = speclist[0] + shutil.copy(spec, targetdir) + finally: + if os.path.isdir(tmpdir): + shutil.rmtree(tmpdir) + +def get_srpm(pkgdirurl, + mode = "current", + targetdirs = None, + version = None, + release = None, + revision = None, + packager = "", + revname = 0, + svnlog = 0, + scripts = []): + svn = SVN() + tmpdir = tempfile.mktemp() + try: + if mode == "version": + geturl = os.path.join(pkgdirurl, "versions", + version, release) + elif mode == "pristine": + geturl = os.path.join(pkgdirurl, "pristine") + elif mode == "current" or mode == "revision": + geturl = os.path.join(pkgdirurl, "current") + else: + raise Error, "unsupported get_srpm mode: %s" % mode + svn.checkout(geturl, tmpdir, rev=revision) + srpmsdir = os.path.join(tmpdir, "SRPMS") + os.mkdir(srpmsdir) + specsdir = os.path.join(tmpdir, "SPECS") + speclist = glob.glob(os.path.join(specsdir, "*.spec")) + if not speclist: + raise Error, "no spec files found" + spec = speclist[0] + if svnlog: + specfile_svn2rpm(pkgdirurl, spec, revision) + revision = svn.revision(tmpdir) + for script in scripts: + status, output = execcmd(script, tmpdir, spec, str(revision), + noerror=1) + if status != 0: + raise Error, "script %s failed" % script + if packager: + packager = " --define 'packager %s'" % packager + execcmd("rpm -bs --nodeps --define '_topdir %s'%s %s" % + (tmpdir, packager, spec)) + if revname: + srpm = glob.glob(os.path.join(srpmsdir, "*.src.rpm"))[0] + srpmbase = os.path.basename(srpm) + os.rename(srpm, "%s/@%i:%s" % (srpmsdir, revision, srpmbase)) + srpm = glob.glob(os.path.join(srpmsdir, "*.src.rpm"))[0] + if not targetdirs: + targetdirs = (".",) + for targetdir in targetdirs: + execcmd("cp -f", srpm, targetdir) + os.unlink(srpm) + finally: + if os.path.isdir(tmpdir): + shutil.rmtree(tmpdir) + +def patch_spec(pkgdirurl, patchfile, log=""): + svn = SVN() + tmpdir = tempfile.mktemp() + try: + geturl = "/".join([pkgdirurl, "current", "SPECS"]) + svn.checkout(geturl, tmpdir) + speclist = glob.glob(os.path.join(tmpdir, "*.spec")) + if not speclist: + raise Error, "no spec files found" + spec = speclist[0] + status, output = execcmd("patch", spec, patchfile) + if status != 0: + raise Error, "can't apply patch:\n%s\n" % output + else: + svn.commit(tmpdir, log="") + finally: + if os.path.isdir(tmpdir): + shutil.rmtree(tmpdir) + +def put_srpm(pkgdirurl, srpmfile, appendname=0, log=""): + srpm = SRPM(srpmfile) + if appendname: + pkgdirurl = "/".join([pkgdirurl, srpm.name]) + svn = SVN() + tmpdir = tempfile.mktemp() + try: + if srpm.epoch: + version = "%s:%s" % (srpm.epoch, srpm.version) + else: + version = srpm.version + versionurl = "/".join([pkgdirurl, "releases", version]) + releaseurl = "/".join([versionurl, srpm.release]) + ret = svn.mkdir(pkgdirurl, noerror=1, log="Created package directory") + if ret: + svn.checkout(pkgdirurl, tmpdir) + svn.mkdir(os.path.join(tmpdir, "releases")) + svn.mkdir(os.path.join(tmpdir, "releases", version)) + svn.mkdir(os.path.join(tmpdir, "current")) + svn.mkdir(os.path.join(tmpdir, "current", "SPECS")) + svn.mkdir(os.path.join(tmpdir, "current", "SOURCES")) + #svn.commit(tmpdir,log="Created package structure.") + version_exists = 1 + currentdir = os.path.join(tmpdir, "current") + else: + if svn.ls(releaseurl, noerror=1): + raise Error, "release already exists" + svn.checkout("/".join([pkgdirurl, "current"]), tmpdir) + svn.mkdir(versionurl, noerror=1, + log="Created directory for version %s." % version) + currentdir = tmpdir + + specsdir = os.path.join(currentdir, "SPECS") + sourcesdir = os.path.join(currentdir, "SOURCES") + + unpackdir = tempfile.mktemp() + os.mkdir(unpackdir) + try: + srpm.unpack(unpackdir) + + uspecsdir = os.path.join(unpackdir, "SPECS") + usourcesdir = os.path.join(unpackdir, "SOURCES") + + uspecsentries = os.listdir(uspecsdir) + usourcesentries = os.listdir(usourcesdir) + specsentries = os.listdir(specsdir) + sourcesentries = os.listdir(sourcesdir) + + # Remove old entries + for entry in [x for x in specsentries + if x not in uspecsentries]: + if entry == ".svn": + continue + entrypath = os.path.join(specsdir, entry) + os.unlink(entrypath) + svn.remove(entrypath) + for entry in [x for x in sourcesentries + if x not in usourcesentries]: + if entry == ".svn": + continue + entrypath = os.path.join(sourcesdir, entry) + os.unlink(entrypath) + svn.remove(entrypath) + + # Copy all files + execcmd("cp -rf", uspecsdir, currentdir) + execcmd("cp -rf", usourcesdir, currentdir) + + # Add new entries + for entry in [x for x in uspecsentries + if x not in specsentries]: + entrypath = os.path.join(specsdir, entry) + svn.add(entrypath) + for entry in [x for x in usourcesentries + if x not in sourcesentries]: + entrypath = os.path.join(sourcesdir, entry) + svn.add(entrypath) + finally: + if os.path.isdir(unpackdir): + shutil.rmtree(unpackdir) + + svn.commit(tmpdir, log=log) + finally: + if os.path.isdir(tmpdir): + shutil.rmtree(tmpdir) + + # Do revision and pristine tag copies + pristineurl = os.path.join(pkgdirurl, "pristine") + svn.remove(pristineurl, noerror=1, + log="Removing previous pristine/ directory.") + currenturl = os.path.join(pkgdirurl, "current") + svn.copy(currenturl, pristineurl, + log="Copying release %s-%s to pristine/ directory." % + (version, srpm.release)) + svn.copy(currenturl, releaseurl, + log="Copying release %s-%s to releases/ directory." % + (version, srpm.release)) + +def create_package(pkgdirurl, log="", verbose=0): + svn = SVN() + tmpdir = tempfile.mktemp() + try: + basename = os.path.basename(pkgdirurl) + if verbose: + print "Creating package directory...", + sys.stdout.flush() + ret = svn.mkdir(pkgdirurl, + log="Created package directory for '%s'." % basename) + if verbose: + print "done" + print "Checking it out...", + svn.checkout(pkgdirurl, tmpdir) + if verbose: + print "done" + print "Creating package structure...", + svn.mkdir(os.path.join(tmpdir, "current")) + svn.mkdir(os.path.join(tmpdir, "current", "SPECS")) + svn.mkdir(os.path.join(tmpdir, "current", "SOURCES")) + if verbose: + print "done" + print "Committing...", + svn.commit(tmpdir, + log="Created package structure for '%s'." % basename) + print "done" + finally: + if os.path.isdir(tmpdir): + shutil.rmtree(tmpdir) + +def mark_release(pkgdirurl, version, release, revision): + svn = SVN() + releasesurl = "/".join([pkgdirurl, "releases"]) + versionurl = "/".join([releasesurl, version]) + releaseurl = "/".join([versionurl, release]) + if svn.ls(releaseurl, noerror=1): + raise cncrep.Error, "release already exists" + svn.mkdir(releasesurl, noerror=1, + log="Created releases directory.") + svn.mkdir(versionurl, noerror=1, + log="Created directory for version %s." % version) + pristineurl = os.path.join(pkgdirurl, "pristine") + svn.remove(pristineurl, noerror=1, + log="Removing previous pristine/ directory.") + currenturl = os.path.join(pkgdirurl, "current") + svn.copy(currenturl, pristineurl, + log="Copying release %s-%s to pristine/ directory." % + (version, release)) + svn.copy(currenturl, releaseurl, rev=revision, + log="Copying release %s-%s to releases/ directory." % + (version, release)) + +def check_changed(url, all=0, show=0, verbose=0): + svn = SVN() + if all: + baseurl = url + packages = [] + if verbose: + print "Getting list of packages...", + sys.stdout.flush() + packages = [x[:-1] for x in svn.ls(baseurl)] + if verbose: + print "done" + if not packages: + raise Error, "couldn't get list of packages" + else: + baseurl, basename = os.path.split(url) + packages = [basename] + clean = [] + changed = [] + nopristine = [] + nocurrent = [] + for package in packages: + pkgdirurl = os.path.join(baseurl, package) + current = os.path.join(pkgdirurl, "current") + pristine = os.path.join(pkgdirurl, "pristine") + if verbose: + print "Checking package %s..." % package, + sys.stdout.flush() + if not svn.ls(current, noerror=1): + if verbose: + print "NO CURRENT" + nocurrent.append(package) + elif not svn.ls(pristine, noerror=1): + if verbose: + print "NO PRISTINE" + nopristine.append(package) + else: + diff = svn.diff(pristine, current) + if diff: + changed.append(package) + if verbose: + print "CHANGED" + if show: + print diff + else: + if verbose: + print "clean" + clean.append(package) + if verbose: + if not packages: + print "No packages found!" + elif all: + print "Total clean packages: %s" % len(clean) + print "Total CHANGED packages: %d" % len(changed) + print "Total NO CURRENT packages: %s" % len(nocurrent) + print "Total NO PRISTINE packages: %s" % len(nopristine) + return {"clean": clean, + "changed": changed, + "nocurrent": nocurrent, + "nopristine": nopristine} + +def checkout(url, path=None, revision=None): + svn = SVN() + current = os.path.join(url, "current") + if path is None: + _, path = os.path.split(url) + svn.checkout(current, path, rev=revision, show=1) + +def get_submit_info(path): + path = os.path.abspath(path) + + # First, look for SPECS and SOURCES directories. + found = False + while path != "/": + if os.path.isdir(path): + specsdir = os.path.join(path, "SPECS") + sourcesdir = os.path.join(path, "SOURCES") + if os.path.isdir(specsdir) and os.path.isdir(sourcesdir): + found = True + break + path = os.path.dirname(path) + if not found: + raise Error, "SPECS and/or SOURCES directories not found" + + # Then, check if this is really a subversion directory. + if not os.path.isdir(os.path.join(path, ".svn")): + raise Error, "subversion directory not found" + + svn = SVN() + + # Now, extract the package name. + for line in svn.info(path): + if line.startswith("URL: "): + url = line.split()[1] + toks = url.split("/") + if len(toks) < 2 or toks[-1] != "current": + raise Error, "unexpected URL received from 'svn info'" + name = toks[-2] + break + else: + raise Error, "URL tag not found in 'svn info' output" + + # Finally, guess revision. + max = -1 + files = [] + files.extend(glob.glob("%s/*" % specsdir)) + files.extend(glob.glob("%s/*" % sourcesdir)) + for line in svn.info(" ".join(files)): + if line.startswith("Revision: "): + rev = int(line.split()[1]) + if rev > max: + max = rev + if max == -1: + raise Error, "revision tag not found in 'svn info' output" + + return name, max + +# vim:et:ts=4:sw=4 diff --git a/RepSys/svn.py b/RepSys/svn.py new file mode 100644 index 0000000..6a42e6d --- /dev/null +++ b/RepSys/svn.py @@ -0,0 +1,349 @@ +from RepSys import Error, config, pexpect +from RepSys.util import execcmd +import sys +import re +import time + +__all__ = ["SVN", "SVNLook", "SVNLogEntry"] + +class SVNLogEntry: + def __init__(self, revision, author, date): + self.revision = revision + self.author = author + self.date = date + self.lines = [] + + def __cmp__(self, other): + return cmp(self.date, other.date) + +class SVN: + def __init__(self, username=None, password=None): + if not username: + username = config.get("auth", "username") + if not password: + password = config.get("auth", "password") + + self.username = username + self.password = password + + def _execsvn(self, *args, **kwargs): + cmdstr = "svn "+" ".join(args) + if kwargs.get("local"): + return execcmd(cmdstr, **kwargs) + show = kwargs.get("show") + noerror = kwargs.get("noerror") + p = pexpect.spawn(cmdstr, timeout=1) + p.setmaxread(1024) + p.setecho(False) + outlist = [] + while True: + i = p.expect_exact([pexpect.EOF, pexpect.TIMEOUT, + "username:", "password:", + "(p)ermanently?", + "Authorization failed"]) + if i == 0: + if show and p.before: + print p.before, + outlist.append(p.before) + break + elif i == 1: + if show and p.before: + print p.before, + outlist.append(p.before) + elif i == 2: + p.sendline(self.username) + outlist = [] + elif i == 3: + p.sendline(self.password) + outlist = [] + elif i == 4: + p.sendline("p") + outlist = [] + elif i == 5: + if not noerror: + raise Error, "authorization failed" + else: + break + while p.isalive(): + try: + time.sleep(1) + except (pexpect.TIMEOUT, pexpect.EOF): + # Continue until the child dies + pass + status, output = p.exitstatus, "".join(outlist).strip() + if status != 0 and not kwargs.get("noerror"): + sys.stderr.write(cmdstr) + sys.stderr.write("\n") + sys.stderr.write(output) + sys.stderr.write("\n") + raise Error, "command failed: "+cmdstr + return status, output + + def _execsvn_success(self, *args, **kwargs): + status, output = self._execsvn(*args, **kwargs) + return status == 0 + + def _add_log(self, cmd_args, received_kwargs, optional=0): + if (not optional or + received_kwargs.has_key("log") or + received_kwargs.has_key("logfile")): + ret = received_kwargs.get("log") + if ret is not None: + cmd_args.append("-m '%s'" % ret) + ret = received_kwargs.get("logfile") + if ret is not None: + cmd_args.append("-F '%s'" % ret) + + def _add_revision(self, cmd_args, received_kwargs, optional=0): + if not optional or received_kwargs.has_key("rev"): + ret = received_kwargs.get("rev") + if isinstance(ret, basestring): + try: + ret = int(ret) + except ValueError: + raise Error, "invalid revision provided" + if ret: + cmd_args.append("-r %d" % ret) + + def add(self, path, **kwargs): + cmd = ["add", path] + return self._execsvn_success(noauth=1, *cmd, **kwargs) + + def copy(self, pathfrom, pathto, **kwargs): + cmd = ["copy", pathfrom, pathto] + self._add_revision(cmd, kwargs, optional=1) + self._add_log(cmd, kwargs) + return self._execsvn_success(*cmd, **kwargs) + + def remove(self, path, force=0, **kwargs): + cmd = ["remove", path] + self._add_log(cmd, kwargs) + if force: + cmd.append("--force") + return self._execsvn_success(*cmd, **kwargs) + + def mkdir(self, path, **kwargs): + cmd = ["mkdir", path] + self._add_log(cmd, kwargs) + return self._execsvn_success(*cmd, **kwargs) + + def commit(self, path, **kwargs): + cmd = ["commit", path] + self._add_log(cmd, kwargs) + return self._execsvn_success(*cmd, **kwargs) + + def checkout(self, url, targetpath, **kwargs): + cmd = ["checkout", "'%s'" % url, targetpath] + self._add_revision(cmd, kwargs, optional=1) + return self._execsvn_success(*cmd, **kwargs) + + def propset(self, propname, value, targets, **kwargs): + cmd = ["propset", propname, "'%s'" % value, targets] + return self._execsvn_success(*cmd, **kwargs) + + def propedit(self, propname, target, **kwargs): + cmd = ["propedit", propname, target] + if kwargs.get("rev"): + cmd.append("--revprop") + self._add_revision(cmd, kwargs) + return self._execsvn_success(local=True, show=True, *cmd, **kwargs) + + def revision(self, path, **kwargs): + cmd = ["info", path] + status, output = self._execsvn(local=True, *cmd, **kwargs) + if status == 0: + for line in output.splitlines(): + if line.startswith("Revision: "): + return int(line.split()[1]) + return None + + def info(self, path, **kwargs): + cmd = ["info", path] + status, output = self._execsvn(local=True, *cmd, **kwargs) + if status == 0: + return output.splitlines() + return None + + def ls(self, path, **kwargs): + cmd = ["ls", path] + status, output = self._execsvn(*cmd, **kwargs) + if status == 0: + return output.split() + return None + + def status(self, path, **kwargs): + cmd = ["status", path] + if kwargs.get("verbose"): + cmd.append("-v") + status, output = self._execsvn(*cmd, **kwargs) + if status == 0: + return [x.split() for x in output.splitlines()] + return None + + def cleanup(self, path, **kwargs): + cmd = ["cleanup", path] + return self._execsvn_success(*cmd, **kwargs) + + def revert(self, path, **kwargs): + cmd = ["revert", path] + status, output = self._execsvn(*cmd, **kwargs) + if status == 0: + return [x.split() for x in output.split()] + return None + + def update(self, path, **kwargs): + cmd = ["update", path] + self._add_revision(cmd, kwargs, optional=1) + status, output = self._execsvn(*cmd, **kwargs) + if status == 0: + return [x.split() for x in output.split()] + return None + + def merge(self, url1, url2=None, rev1=None, rev2=None, path=None, **kwargs): + cmd = ["merge"] + if rev1 and rev2 and not url2: + cmd.append("-r") + cmd.append("%s:%s" % (rev1, rev2)) + cmd.append(url1) + else: + if not url2: + raise ValueError, \ + "url2 needed if two revisions are not provided" + if rev1: + cmd.append("%s@%s" % (url1, rev1)) + else: + cmd.append(url1) + if rev2: + cmd.append("%s@%s" % (url2, rev2)) + else: + cmd.append(url2) + if path: + cmd.append(path) + status, output = self._execsvn(*cmd, **kwargs) + if status == 0: + return [x.split() for x in output.split()] + return None + + def diff(self, pathurl1, pathurl2=None, **kwargs): + cmd = ["diff", pathurl1] + self._add_revision(cmd, kwargs, optional=1) + if pathurl2: + cmd.append(pathurl2) + status, output = self._execsvn(*cmd, **kwargs) + if status == 0: + return output + return None + + def cat(self, url, **kwargs): + cmd = ["cat", url] + self._add_revision(cmd, kwargs, optional=1) + status, output = self._execsvn(*cmd, **kwargs) + if status == 0: + return output + return None + + def log(self, url, start=None, end=0, **kwargs): + cmd = ["log", url] + if start is not None: + if type(start) is not type(0): + try: + start = int(start) + except (ValueError, TypeError): + raise Error, "invalid log start revision provided" + if type(end) is not type(0): + try: + end = int(end) + except (ValueError, TypeError): + raise Error, "invalid log end revision provided" + cmd.append("-r %d:%d" % (start, end)) + status, output = self._execsvn(*cmd, **kwargs) + if status != 0: + return None + + revheader = re.compile("^r(?P<revision>[0-9]+) \| (?P<author>.+?) \| (?P<date>.+?) \| (?P<lines>[0-9]+) (?:line|lines)") + logseparator = "-"*72 + linesleft = 0 + entry = None + log = [] + emptyline = 0 + for line in output.splitlines(): + if emptyline: + emptyline = 0 + continue + line = line.rstrip() + if linesleft == 0: + if line != logseparator: + m = revheader.match(line) + if m: + linesleft = int(m.group("lines")) + timestr = " ".join(m.group("date").split()[:2]) + timetuple = time.strptime(timestr, + "%Y-%m-%d %H:%M:%S") + entry = SVNLogEntry(int(m.group("revision")), + m.group("author"), timetuple) + log.append(entry) + emptyline = 1 + else: + entry.lines.append(line) + linesleft -= 1 + log.sort() + log.reverse() + return log + +class SVNLook: + def __init__(self, repospath, txn=None, rev=None): + self.repospath = repospath + self.txn = txn + self.rev = rev + + def _execsvnlook(self, cmd, *args, **kwargs): + execcmd_args = ["svnlook", cmd, self.repospath] + self._add_txnrev(execcmd_args, kwargs) + execcmd_args += args + execcmd_kwargs = {} + keywords = ["show", "noerror"] + for key in keywords: + if kwargs.has_key(key): + execcmd_kwargs[key] = kwargs[key] + return execcmd(*execcmd_args, **execcmd_kwargs) + + def _add_txnrev(self, cmd_args, received_kwargs): + if received_kwargs.has_key("txn"): + txn = received_kwargs.get("txn") + if txn is not None: + cmd_args += ["-t", txn] + elif self.txn is not None: + cmd_args += ["-t", self.txn] + if received_kwargs.has_key("rev"): + rev = received_kwargs.get("rev") + if rev is not None: + cmd_args += ["-r", rev] + elif self.rev is not None: + cmd_args += ["-r", self.rev] + + def changed(self, **kwargs): + status, output = self._execsvnlook("changed", **kwargs) + if status != 0: + return None + changes = [] + for line in output.splitlines(): + line = line.rstrip() + if not line: + continue + entry = [None, None, None] + changedata, changeprop, path = None, None, None + if line[0] != "_": + changedata = line[0] + if line[1] != " ": + changeprop = line[1] + path = line[4:] + changes.append((changedata, changeprop, path)) + return changes + + def author(self, **kwargs): + status, output = self._execsvnlook("author", **kwargs) + if status != 0: + return None + return output.strip() + +# vim:et:ts=4:sw=4 diff --git a/RepSys/util.py b/RepSys/util.py new file mode 100644 index 0000000..0337bc1 --- /dev/null +++ b/RepSys/util.py @@ -0,0 +1,54 @@ +#!/usr/bin/python +from RepSys import Error, config +import getpass +import sys, os +#import commands + +# Our own version of commands' getstatusoutput(). We have a commands +# module directory, so we can't import Python's standard module +def commands_getstatusoutput(cmd): + """Return (status, output) of executing cmd in a shell.""" + import os + pipe = os.popen('{ ' + cmd + '; } 2>&1', 'r') + text = pipe.read() + sts = pipe.close() + if sts is None: sts = 0 + if text[-1:] == '\n': text = text[:-1] + return sts, text + +def execcmd(*cmd, **kwargs): + cmdstr = " ".join(cmd) + if kwargs.get("show"): + status = os.system(cmdstr) + output = "" + else: + status, output = commands_getstatusoutput("LANG=C "+cmdstr) + if status != 0 and not kwargs.get("noerror"): + raise Error, "command failed: %s\n%s\n" % (cmdstr, output) + if config.getbool("global", "verbose", 0): + print cmdstr + sys.stdout.write(output) + return status, output + +def get_auth(username=None, password=None): + set_username = 1 + set_password = 1 + if not username: + username = config.get("auth", "username") + if not username: + username = raw_input("username: ") + else: + set_username = 0 + if not password: + password = config.get("auth", "password") + if not password: + password = getpass.getpass("password: ") + else: + set_password = 0 + if set_username: + config.set("auth", "username", username) + if set_password: + config.set("auth", "password", password) + return username, password + +# vim:et:ts=4:sw=4 @@ -0,0 +1,55 @@ +#!/usr/bin/python +from RepSys import Error +from RepSys.command import * +import getopt +import sys + +VERSION="1.5.3" + +HELP = """\ +Usage: repsys COMMAND [COMMAND ARGUMENTS] + +Useful commands: + co + submit + create + getspec + getsrpm + changed + authoremail + +Run "repsys COMMAND --help" for more information. + +Written by Gustavo Niemeyer <gustavo@niemeyer.net> +""" + +def parse_options(): + parser = OptionParser(help=HELP, version="%prog "+VERSION) + parser.disable_interspersed_args() + parser.add_option("--debug", action="store_true") + opts, args = parser.parse_args() + if len(args) < 1: + parser.print_help(sys.stderr) + sys.exit(1) + opts.command = args[0] + opts.argv = args + return opts + +def dispatch_command(command, argv, debug=0): + sys.argv = argv + try: + repsys_module = __import__("RepSys.commands."+command) + commands_module = getattr(repsys_module, "commands") + command_module = getattr(commands_module, command) + except (ImportError, AttributeError): + if debug: + import traceback + traceback.print_exc() + sys.exit(1) + raise Error, "invalid command '%s'" % command + command_module.main() + +if __name__ == "__main__": + do_command(parse_options, dispatch_command) + +# vim:et:ts=4:sw=4 diff --git a/repsys.conf b/repsys.conf new file mode 100644 index 0000000..05b4890 --- /dev/null +++ b/repsys.conf @@ -0,0 +1,22 @@ + +[global] +#verbose = true +default_parent = svn+ssh://cvs.mandriva.com/svn/mdv/cooker/ + +[auth] +#username = foo +#password = bar + +[log] +concat = mapi2 +oldurl = svn+ssh://cvs.mandriva.com/svn/mdv/misc + +[users] +helio = Helio Chissini de Castro <helio@mandriva.com> + +[submit] +name = cooker +target = /teste-root/testadora/entrada/ +allowed = svn+ssh://cvs.mandriva.com/svn/mdv/cooker/ +# scripts = /svnroot/cnc.admin/rebrand-cl + diff --git a/repsys.spec b/repsys.spec new file mode 100644 index 0000000..9805508 --- /dev/null +++ b/repsys.spec @@ -0,0 +1,143 @@ +%define py_ver %(python -c "import sys; v=sys.version_info[:2]; print '%%d.%%d'%%v" 2>/dev/null || echo PYTHON-NOT-FOUND) +%define maxver %(python -c "import sys; a,b=sys.version_info[:2]; print%'%%d.%%d'%%(a,b+1)" 2>/dev/null || echo PYTHON-NOT-FOUND) +%define minver %py_ver +%define py_prefix %(python -c "import sys; print sys.prefix" 2>/dev/null || echo PYTHON-NOT-FOUND) +%define py_libdir %{py_prefix}/%{_lib}/python%{py_ver} +%define py_sitedir %{py_libdir}/site-packages + +%define repsys_version 1.5.3 + +Name: repsys +Version: 1.5.3.1 +Release: 4mdk +Summary: Tools for Mandriva Linux repository access and management +Group: Development/Other +Source: %{name}-%{repsys_version}.tar.gz +Source1: mdk-repsys.conf +Source2: mdk-rebrand-mdk +# Direct wrapper for get srpm with release based on svn number +Source3: getsrpm-mdk +Patch0: repsys-mdk.patch +Patch1: mdk-changelog-repsys-1.5.3.patch +License: GPL +URL: http://qa.mandriva.com/twiki/bin/view/Main/RepositorySystem +Prefix: %{_prefix} +Buildrequires: python-devel +BuildRoot: %{_tmppath}/%{name}-%{version}-root +BuildRequires: python +BuildRequires: python-devel + +%description +Tools for Mandriva Linux repository access and management. + +%prep +%setup -q -n %{name}-%{repsys_version} +%patch0 -p1 +%patch1 -p1 + +%build +python setup.py build + +%install +rm -rf %{buildroot} + +python setup.py install --root=%{buildroot} +# Using compile inline since niemeyer's python macros still not available on mdk rpm macros +find %{buildroot}%{py_sitedir} -name '*.pyc' -exec rm -f {} \; +python -c "import sys, os, compileall; br='%{buildroot}'; compileall.compile_dir(sys.argv[1], ddir=br and +(sys.argv[1][len(os.path.abspath(br)):]+'/') or None)" %{buildroot}%{py_sitedir} + +mkdir -p %{buildroot}%{_sysconfdir} +mkdir -p %{buildroot}%{_datadir}/repsys/ +mkdir -p %{buildroot}%{_bindir}/ +cp %{SOURCE1} %{buildroot}%{_sysconfdir}/repsys.conf +cp %{SOURCE2} %{buildroot}%{_datadir}/repsys/rebrand-mdk +cp %{SOURCE3} %{buildroot}%{_bindir}/getsrpm-mdk + +%clean +rm -rf %{buildroot} + +%files +%defattr(0644,root,root,0755) +%doc repsys.conf +%attr(0644,root,root) %config(noreplace) %{_sysconfdir}/repsys.conf +%defattr(0755,root,root,0755) +%{_bindir}/repsys +%{_bindir}/getsrpm-mdk +%{_datadir}/repsys/rebrand-mdk +%{py_sitedir}/RepSys + + + +%changelog +* Fri Sep 30 2005 Andreas Hasenack <andreas@mandriva.com> ++ 2005-09-30 18:25:48 (979) +- releasing 1.5.3.1-4mdk + +* Fri Sep 30 2005 Andreas Hasenack <andreas@mandriva.com> ++ 2005-09-30 18:10:53 (978) +- fixed author's email +- fixed mandriva logo url + +* Fri Sep 30 2005 Andreas Hasenack <andreas@mandriva.com> ++ 2005-09-30 17:41:45 (977) +- fixed mime-type of the repsys-mdk.patch + +* Tue Jul 26 2005 Helio Chissini de Castro <helio@mandriva.com> ++ 2005-07-26 04:48:46 (441) +- Changes on behalf of Oden Eriksson +- update S1 +- lib64 fixes +- this is no noarch package +- rpmlint fixes + +* Wed Jun 29 2005 Helio Chissini de Castro <helio@mandriva.com> ++ 2005-06-29 04:50:47 (257) +- Upload new spec + +* Wed Jun 29 2005 Helio Chissini de Castro <helio@mandriva.com> ++ 2005-06-29 04:44:48 (256) +- Fixed ugly type on url type svn+ssh + +* Tue Jun 28 2005 Helio Chissini de Castro <helio@mandriva.com> ++ 2005-06-28 07:22:47 (248) +- Update repsys to match new changelog requirements ( just release keep unchanged ) +- Update getsrpm-mdk to genrate srpm with changelog +- Fixed regexp for unicode/color chars in terminal ( thanks to aurelio ) + +* Tue Jun 14 2005 Helio Chissini de Castro <helio@mandriva.com> ++ 2005-06-14 05:04:31 (206) +- Start to fix builds on x86_64 archs. + +* Wed Jun 08 2005 Helio Chissini de Castro <helio@mandriva.com> ++ 2005-06-08 04:48:55 (151) +- Fixed patch for get real changelog and version + +* Sun May 29 2005 Helio Chissini de Castro <helio@mandriva.com> ++ 2005-05-29 13:08:25 (147) +- Added changelog patch to match mdk style + +* Fri May 27 2005 Helio Chissini de Castro <helio@mandriva.com> ++ 2005-05-27 10:26:17 (146) +- Added rebrand script for match release number with svn +- Added wrapper script for get srpms ready for submit to cluster compilation + +* Fri May 27 2005 Helio Chissini de Castro <helio@mandriva.com> ++ 2005-05-27 09:46:09 (145) +- Added suggested changes by neoclust + +* Fri May 27 2005 Helio Chissini de Castro <helio@mandriva.com> ++ 2005-05-27 04:23:34 (144) +- Added initial users on default + +* Wed May 25 2005 Helio Chissini de Castro <helio@mandriva.com> ++ 2005-05-25 11:10:18 (143) +- Added a initial changelog until repsys submit is working + +* Wed May 25 2005 Helio Chissini de Castro <helio@mandriva.com> ++ 2005-05-25 10:28:57 (142) +- No bziped patches + +* Wed May 25 2005 Helio Chissini de Castro <helio@mandriva.com> ++ 2005-05-25 10:22:47 (141) +- Initial import of repsys package diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..5baf7c1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[bdist_rpm] +doc_files = repsys.conf + diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..2ff3507 --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +#!/usr/bin/python +from distutils.core import setup +import sys +import re + +verpat = re.compile("VERSION *= *\"(.*)\"") +data = open("repsys").read() +m = verpat.search(data) +if not m: + sys.exit("error: can't find VERSION") +VERSION = m.group(1) + +setup(name="repsys", + version = VERSION, + description = "Tools for Mandriva Linux repository access and management", + author = "Gustavo Niemeyer", + author_email = "gustavo@niemeyer.net", + url = "http://qa.mandriva.com/twiki/bin/view/Main/RepositorySystem", + license = "GPL", + long_description = """Tools for Mandriva Linux repository access and management.""", + packages = ["RepSys", "RepSys.cgi", "RepSys.commands"], + scripts = ["repsys"] + ) |