#!/usr/bin/env python # # shipper -- a tool for shipping software import sys, os, readline, re, commands, time, glob, optparse, stat # # State variables # destinations = [] # List of remote directories to update channels = ['ibiblio', 'redhat', 'freshmeat'] whoami = None # Who am I? (Used for FTP logins) date = None # User has not yet set a date package = None # Nor a package name homepage = None # Nor a home page arch = None # The machine architecture keywords = None # Keywords for LSMs freshmeat_name = None # Name of the project ob Freshmeat changelog = None # Project changelog lastchange = None # Last entry in changelog summary = None # One-line summary of the package description = None # Nor a description indextemplate = """
%(description)s
Last modified %(date)s.
""" mailtemplate = """Subject: Announcing release %(version)s of %(package)s Release %(version)s of %(package)s is now available at: %(homepage)s Here are the most recent changes: %(lastchange)s -- shipper, acting for %(whoami)s """ # It's unpleasant that we have to include these here, but # the freshmeat release focus has to be validated even if the # user is offline and the XML-RPC service not accessible. freshmeat_focus_types = ( "N/A", "Initial freshmeat announcement", "Documentation", "Code cleanup", "Minor feature enhancements", "Major feature enhancements", "Minor bugfixes", "Major bugfixes", "Minor security fixes", "Major security fixes", ) def croak(msg): sys.stderr.write("shipper: " + msg + "\n") sys.exit(1) # # Shipping methods # def do_or_die(cmd): "Wither execute a command or fail noisily" if options.verbose: print "***", cmd if os.system(cmd): croak("command '%s' failed!" % cmd) def upload_or_die(cmd): if options.noupload: print cmd else: do_or_die(cmd) def upload(destination, files): # Upload a file via ftp or sftp, handles print "# Uploading to %s" % destination files = filter(os.path.exists, files) if destination.startswith("ftp://"): destination = destination[6:].split("/") host = destination.pop(0) directory = "/".join(destination) commands = ["lftp", "open -u anonymous," + whoami + " " + host + "\n"] if directory: commands.append("cd " + directory + "\n") commands.append("mput " + " ".join(files) + "\n") commands.append("close\n") if options.noupload: print "".join(commands) else: pfp = os.popen(commands.pop(0), "w") pfp.writelines(commands) pfp.close() elif destination.find("::") > -1: upload_or_die("rsync " + " ".join(files) + " " + destination) elif destination.find(":") > -1: (host, directory) = destination.split(":") for file in files: # This is a really ugly way to deal with the problem # of write-protected files in the remote directory. # Unfortunately, sftp(1) is rather brain-dead -- no # way to ignore failure on a remove, and refuses to # do renames with an obscure error message. remote = os.path.join(directory, package, file) upload_or_die("scp " + file + " " + host + ":" + remote+".new;") upload_or_die("ssh %s 'mv -f %s.new %s'" % (host, remote, remote)) else: sys.stderr.write("Don't know what to do with destination %s!") def freshmeat_ship(manifest): "Ship a specified update to freshmeat." if options.verbose: print "Announcing to freshmeat..." upload_or_die("freshmeat-submit <" + manifest[0]) # # Metadata extraction # def grep(pattern, file): "Mine for a specified pattern in a file." fp = open(file) try: while True: line = fp.readline() if not line: return None m = re.search(pattern, line) if m: return m.group(1) finally: fp.close() return None class Specfile: def __init__(self, filename): self.filename = filename self.type = None if filename.endswith(".spec"): self.type = "RPM" self.package = self.extract("Name") self.version = self.extract("Version") self.homepage = self.extract("URL") self.summary = self.extract("Summary") self.arch = self.extract("BuildArch") or commands.getoutput("rpm --showrc | sed -n '/^build arch/s/.* //p'") self.description = self.rpm_get_multiline("description") self.changelog = self.rpm_get_multiline("changelog") elif filename == "control": self.type = "deb" self.name = self.extract("Package") self.version = self.extract("Version").split("-")[0] self.homepage = self.extract("XBS-Home-Page") self.summary = self.extract("Description") self.arch = self.extract("Architecture") if not self.arch: croak("this control file lacks an Architecture field") # FIXME: parse Debian description entries and changelog file self.description = self.changelog = None def extract(self, fld): "Extract a one-line field, possibly embedded as a magic comment." if self.type == "RPM": return grep("^#?"+fld+":\s*(.*)", self.filename) elif self.type == "deb": return grep("^(?:XBS-)?"+fld+": (.*)", self.filename) def rpm_get_multiline(self, fieldname): "Grab everything from leader line to just before the next leader line." global desc fp = open(self.filename) desc = "" gather = False while True: line = fp.readline() if not line: break # Pick up fieldnames *without* translation options. if line.strip() == "%" + fieldname: gather = True continue elif line[0] == "%": gather = False if gather: desc += line fp.close() if desc: return desc.strip() + "\n" else: return None # # Main sequence # try: # # Process options # parser = optparse.OptionParser(usage="%prog: [-h] [-n] [-f] [-v]") parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="print progress messages to stdout") parser.add_option("-n", "--noupload", action="store_true", dest="noupload", default=False, help="don't do uploads, just build deliverables") parser.add_option("-N", "--nobuild", action="store_true", dest="nobuild", default=False, help="dump configuration only, no builds or uploads") parser.add_option("-f", "--force", action="store_true", dest="force", default=False, help="force rebuilding of all local deliverables") (options, args) = parser.parse_args() # # Extract metadata and compute control information # def disable(s): channels.remove(s) # Security check, don't let an attacker elevate privileges def securecheck(file): if stat.S_IMODE(os.stat(file).st_mode) & 00002: croak("%s must not be world-writeable!" % file) # Read in variable overrides securecheck(".") home_profile = os.path.join(os.getenv('HOME'), ".shipper") if os.path.exists(home_profile): securecheck(home_profile) execfile(home_profile) here_profile = ".shipper" if os.path.exists(here_profile): securecheck(here_profile) execfile(here_profile) # Set various sensible defaults if not whoami: whoami = os.getenv('USERNAME') + "@" + os.getenv('HOSTNAME') # Where to get the metadata specfiles = glob.glob("*.spec") if len(specfiles) == 1: metadata = Specfile(specfiles[0]) elif os.path.exists("control"): metadata = Specfile("control") else: croak("must be exactly one RPM or dpkg specfile in the directory!") # Get the package name if not package: package = metadata.package if not package: croak("can't get package name!") # Extract the package vers from the specfile or Makefile specvers = metadata.version makevers = None if os.path.exists("Makefile"): makevers = grep("^VERS[A-Z]* *= *(.*)", "Makefile") # Maybe it's a shell command intended to extract version from specfile if makevers and makevers[0] == '$': makevers = commands.getoutput(makevers[7:-1]) if specvers != makevers: croak("specfile version %s != Makefile version %s"%(specvers,makevers)) elif specvers == None: croak("can't get package version") elif specvers[0] not in "0123456789": croak("package version %s appears garbled" % specvers) else: version = specvers # Specfiles may set their own destinations local_destinations = metadata.extract("Destinations") if local_destinations: local_destinations = map(lambda x: x.strip(), local_destinations.split(",")) destinations += local_destinations if not destinations: print "warning: destinations empty, shipping to public channels only." print"# Uploading version %s of %s" % (version, package) # Extract remaining variables for templating if not homepage: homepage = metadata.homepage if not date: date = time.asctime() if not summary: summary = metadata.summary if not description: description = metadata.description if not arch: arch = metadata.arch if not keywords: keywords = metadata.extract("Keywords") if not freshmeat_name: freshmeat_name = metadata.extract("Freshmeat-Name") # Finally, derive the change log and lastchange entry; # we'll need the latter for freshmeat.net freshmeat_lastchange = lastchange = changelog = None # ChangeLog, if present, takes precedence; # we assume if both are present that the specfile log is about packaging. if os.path.exists("ChangeLog"): ifp = open("ChangeLog", "r") changelog = ifp.rea