"Fossies" - the Fresh Open Source Software Archive

Member "shipper-1.18/shipper" (31 Jul 2020, 49486 Bytes) of package /linux/privat/shipper-1.18.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "shipper": 1.17_vs_1.18.

    1 #!/usr/bin/env python
    2 #
    3 # shipper -- a tool for shipping software
    4 #
    5 # SPDX-License-Identifier: BSD-2-Clause
    6 #
    7 # Runs under both Python 2 and Python 3. Preserve this property!
    8 #
    9 from __future__ import print_function
   10 
   11 import sys, os, re, subprocess, time, glob, argparse, stat, email.utils, cgi, copy
   12 
   13 try:
   14     from twython import Twython, TwythonError
   15     tweet_capable = True
   16 except ImportError:
   17     tweet_capable = False
   18 
   19 shipper_version = "1.18"
   20 
   21 # See https://codetheweb.blog/2017/12/05/css-flexboxes/
   22 DEFAULT_HTML_TEMPLATE = """\
   23 <!doctype html>
   24 <head>
   25 <meta charset="utf-8"/>
   26 <meta name='description' content='Resource page for %(project)s' />
   27 <meta name='generator' content='shipper' />
   28 <meta name='description' content='%(summary)s'/>
   29 <title>Resource page for %(project)s %(version)s</title>
   30 <style>
   31 body {
   32     margin: 0;
   33 }
   34 
   35 #Header {
   36     border: 2px solid black;
   37     display: flex;
   38 }
   39 
   40 .headpart {
   41     padding: 5px;
   42     width: 100vh;
   43 }
   44 #div1 {
   45     text-align:left;
   46 }
   47 #div2 {
   48     text-align:center;
   49 }
   50 #div3 {
   51     text-align:right;
   52 }
   53 
   54 #Resources {
   55   border-collapse: collapse;
   56   width: 100%%;
   57 }
   58 
   59 #Resources td {
   60   border: 1px solid #ddd;
   61   padding: 8px;
   62 }
   63 
   64 #Resources tr:nth-child(even){background-color: #f2f2f2;}
   65 
   66 #Resources tr:hover {background-color: #ddd;}
   67 
   68 </style>
   69 </head>
   70 <body%(body_attributes)s>
   71 
   72 <div id="Header">
   73     <div class="headpart" id="div1"><h2>Resource page for %(project)s %(version)s</h2></div>
   74     <div class="headpart" id="div2">%(centertext)s</div>
   75     <div class="headpart" id="div3"><h2>%(date)s</h2></div>
   76 </div>
   77 
   78 <h2>Summary</h2>
   79 
   80 <p>%(summary)s</p>
   81 %(description)s
   82 
   83 <h2>Resources </h2>
   84 
   85 <br />
   86 %(resourcetable)s
   87 <br />
   88 
   89 %(extralines)s
   90 
   91 <h2>Recent Changes</h2>
   92 <pre>
   93 %(lastchange)s
   94 </pre>
   95 
   96 </div>
   97 </body>
   98 </html>
   99 """
  100 
  101 DEFAULT_GITWEB_TEMPLATE = """\
  102 %(description)s\
  103 
  104 <p>Home page: <b><a href='%(website)s'>%(website)s</a></b></p>
  105 
  106 <p>Developer Clone: <b>git clone %(developer_clone)s</b></p>
  107 
  108 <p>Anonymous Clone: <b>git clone %(anonymous_clone)s</b></p>
  109 """
  110 
  111 DEFAULT_EMAIL_TEMPLATE = """\
  112 Subject: Announcing release %(version)s of %(project)s
  113 
  114 Release %(version)s of %(project)s is now available at:
  115 
  116     %(website)s
  117 
  118 Here are the most recent changes:
  119 
  120 %(lastchange)s
  121 --
  122                              shipper, acting for %(whoami)s
  123 """
  124 
  125 newssites = ("freshcode",)
  126 
  127 class siteprops:
  128     def __init__(self):
  129         self.id = None
  130         self.name = None
  131         self.folder = None
  132 
  133 class Shipper:
  134     "Hold shipper state variables so they can be enumerated."
  135     multiline = {'description', 'lastchange',
  136                  'extralines', 'mail_template', 'html_template'}
  137     hardwired = {'savannah', 'sourceforge'}
  138     perproject = {'website', 'irc_channel', 'project_tags', 'freshcode_name',
  139                   'repository_url', 'developer_clone', 'anonymous_clone',
  140                   'openhub_url', 'summary', 'webdir', 'web_extras', 'logo',
  141                   'tag_template', 'tag_message', 'instant_message', 'html_target',
  142                   'validate', 'body_attributes', 'bkgimage', 'debian_packages'}
  143     def __init__(self):
  144         self.destinations = []      # List of remote directories to update
  145         self.gitolite_pattern = None    # How to know a remote is run by gitolite
  146         self.date = time.strftime("%Y-%m-%d", time.gmtime())
  147         self.whoami = None      # Who am I? (Used for logins and mail signatures)
  148         self.patreon_id = None      # My name on patreon.com
  149         # Special-destination properties
  150         self.savannah = siteprops() # Savannah site properties
  151         self.sourceforge = siteprops()  # Sourceforge site properties
  152         # Twitter
  153         self.twitter_consumer_key = None
  154         self.twitter_consumer_secret = None
  155         self.twitter_access_token = None
  156         self.twitter_access_token_secret = None
  157         # Per-project
  158         self.project = os.path.basename(os.getcwd())    # Nor a project name
  159         self.version = None     # Project release version
  160         self.website = None     # Project home page
  161         self.irc_channel = None     # Project IRC channels
  162         self.project_tags = None    # Keywords for tagging
  163         self.freshcode_name = None  # Name of the project on Freshcode
  164         self.repository_url = None  # URL of the repository web interface
  165         self.developer_clone = None # URL for developers to clone from
  166         self.anonymous_clone = None # URL for others to clone from
  167         self.openhub_url = ""       # Where the OpenHub stats live
  168         self.summary = None     # One-line summary of the project
  169         self.webdir = None      # Web directory to be mirrored to a project site
  170         self.web_extras = None      # Extra web deliverables
  171         self.logo = ""          # Project logo
  172         self.tag_template = None    # How to tag releases
  173         self.tag_message = "Tagged for external release %(version)s"
  174         self.instant_message = "%(project)s-%(version)s has just shipped."
  175         self.html_target = None     # Name to template a page to
  176         self.validate = None        # Validation check before shipping
  177         self.body_attributes = ''   # Attributes for body tags
  178         self.bkgimage = ''      # Background image for web page
  179         self.debian_packages = None       # Package names in Debian and derivatives.
  180         # Stuff after this point is multiline
  181         self.description = None     # Multiline description
  182         self.lastchange = None      # Last entry in changelog
  183         self.extralines = ""        # Extra lines for HTML template
  184         self.html_template = DEFAULT_HTML_TEMPLATE
  185         self.gitweb_template = DEFAULT_GITWEB_TEMPLATE
  186         self.mail_template = DEFAULT_EMAIL_TEMPLATE
  187         self.lastchange = None      # Contents of last changelog entry
  188     def incorporate(self, profile):
  189         "Read in a profile in Python syntax."
  190         before = dir()
  191         savannah = siteprops()
  192         sourceforge = siteprops()
  193         exec(open(profile).read())
  194         after = dir()
  195         after.remove('before')
  196         for name in set(after) - set(before):
  197             setattr(self, name, eval(name))
  198         self.savannah = savannah
  199         self.sourceforge = sourceforge
  200         # Allow local nullification in .shipper of what's in ~/.config/shipper
  201         if self.html_target and self.html_template is None:
  202             self.html_template = DEFAULT_HTML_TEMPLATE
  203     def merge_with_negations(self, additional):
  204         "Merge in additional destinations, using ~ for exclusion."
  205         negations = [s for s in additional if s.startswith("~")]
  206         additional = [s for s in additional if not s.startswith("~")]
  207         if "~" in negations:
  208             self.destinations = additional
  209         else:
  210             self.destinations += additional
  211             for removable in [s[1:] for s in negations]:
  212                 if removable in self.destinations:
  213                     self.destinations.remove(removable)
  214     def igor_payload(self):
  215         return "gitlab.com" in self.repository_url
  216     def dump(self):
  217         "Dump the shipper state variables."
  218         for member in sorted(shipper.__dict__.keys()):
  219             if member in Shipper.hardwired:
  220                 for (k, v) in list(getattr(self, member).__dict__.items()):
  221                     if v is not None:
  222                         print("%s.%s = %s" % (member, k, v))
  223             elif member in Shipper.multiline:
  224                 if not getattr(self, member):
  225                     print("%s = None" % member)
  226                 else:
  227                     print("%s = <<EOF\n%sEOF" % (member, getattr(self, member)))
  228             else:
  229                 print("%s = %s" % (member, repr(getattr(self, member))))
  230 
  231 def croak(msg):
  232     sys.stderr.write("shipper: " + msg + "\n")
  233     sys.exit(1)
  234 
  235 try:
  236     getstatusoutput = subprocess.getstatusoutput
  237 except AttributeError:
  238     import commands
  239     getstatusoutput = commands.getstatusoutput
  240 
  241 def capture(cmd):
  242     (s, o) = getstatusoutput(cmd)
  243     if s != 0:
  244         return None
  245     return o.strip()
  246 
  247 #
  248 # Shipping methods
  249 #
  250 
  251 def upload(dest, files, subdir=None):
  252     "Generate upload command for a file via ftp or scp."
  253     if subdir is None:
  254         subdir = shipper.project
  255     files = [x for x in files if os.path.exists(x) or x == shipper.html_target]
  256     if dest.startswith("ftp://"):
  257         dest = dest[6:].split("/")
  258         host = dest.pop(0)
  259         directory = "/".join(dest)
  260         upcmds = ["lftp\n", "open -u anonymous," + myaddress + " " + host + "<<'INAGADADAVIDA'\n"]
  261         if directory:
  262             upcmds.append("cd " + directory + "\n")
  263         upcmds.append("mput " + " ".join(files) + "\n")
  264         upcmds.append("close\n")
  265         upcmds.append("INAGADADAVIDA\n")
  266         print("".join(upcmds))
  267     elif dest.find(":") > -1:
  268         (host, directory) = dest.split(":")
  269         for fn in files:
  270             # Requires your files to be writeable by you - maybe not so if they
  271             # were RCSed but not locked! If they are, the -p option to scp
  272             # will set that permission on the remote host and allow you to
  273             # update the files with scp later.  The -q option suppresses
  274             # /etc/issue display from the remote system.
  275             remote = os.path.join(directory, subdir, os.path.basename(fn))
  276             print("scp -q -p " + fn + " " + host + ":" + remote)
  277     else:
  278         sys.stderr.write("Don't know what to do with destination %s!\n" % dest)
  279 
  280 def make_templated_pages(deliverables, download, options):
  281     "Make templated page, listing specified deliverables."
  282     resourcetable = '<table id="Resources" summary="Downloadable resources">\n'
  283     for (ifile, explanation, __tag, big) in deliverables:
  284         # CSS deliverables don't get displayed
  285         if ifile.endswith("css"):
  286             continue
  287         # Also elide anything asciidoc with a generated web version
  288         if ifile.endswith(".adoc") and os.path.exists(ifile.replace("adoc", "html")):
  289             continue
  290         if big:
  291             indurl = download
  292         else:
  293             indurl = shipper.website
  294         indurl = os.path.join(indurl, ifile)
  295         resourcetable += "<tr><td><a href='%s'>%s</a></td><td>%s</td></tr>\n" % (indurl, ifile, explanation)
  296     if shipper.igor_payload() and options.gitlab_igor:
  297         resourcetable += "<tr><td><a href='%s/-/releases'>Releases</a></td><td>Page of release tarballs</td></tr>\n" % (shipper.repository_url,)
  298     resourcetable += "</table>"
  299     if shipper.repository_url:
  300         shipper.extralines += "<p>The project repository is at <a href='%s'>%s</a>.</p>\n" % \
  301                               (shipper.repository_url, shipper.repository_url)
  302 
  303     if shipper.openhub_url:
  304         shipper.extralines += "<p>Project statistics are available at <a href='%s'>OpenHub</a>.</p>\n" % \
  305                       shipper.openhub_url
  306     if shipper.debian_packages is not None:
  307         shipper.extralines += '<p>This is packaged for Debian and derivatives as: '
  308         shipper.extralines += shipper.debian_packages
  309         shipper.extralines += '</p>\n'
  310     if shipper.irc_channel is not None:
  311         if ',' in shipper.irc_channel:
  312             shipper.extralines += '<p>There are project IRC channels:'
  313             shipper.extralines += shipper.irc_channel
  314             shipper.extralines += '</p>\n'
  315         else:
  316             shipper.extralines += '<p>There is a project <a href="%s">IRC channel</a>.</p>\n' % shipper.irc_channel
  317     if 'freshcode' in shipper.destinations:
  318         shipper.extralines += "<p>There is a Freshcode <a href='http://freshcode.club/projects/%s'>%s page</a>.</p>\n" % \
  319                       (shipper.freshcode_name, shipper.freshcode_name)
  320     if shipper.patreon_id:
  321         shipper.extralines += "<p>If you appreciate this code (and especially if you "
  322         shipper.extralines += "make money by using it) please "
  323         shipper.extralines += "<a href='http://www.patreon.com/%s'>" % shipper.patreon_id
  324         shipper.extralines += "support me on Patreon</a>.</p>"
  325     centertext = ""
  326     substitutions = shipper.__dict__.copy()
  327     if "logo" in substitutions and substitutions["logo"]:
  328         centertext = '<img src="%s"/>' % substitutions["logo"]
  329     substitutions["description"] = "<p>" + substitutions["description"].replace("\n\n", "</p>\n\n<p>") + "</p>"
  330     substitutions.update(locals())
  331     if "pagemaking" in phases and shipper.html_template and shipper.html_target:
  332         # OK, now build the templated page itself
  333         if 'bkgimage' in substitutions and substitutions['bkgimage']:
  334             substitutions['body_attributes'] += (" background='%s'" % substitutions['bkgimage'])
  335         if "web" in phases:
  336             print("cat >%s <<'INAGADADAVIDA'" % shipper.html_target)
  337             sys.stdout.flush()
  338         proc = subprocess.Popen("imgsizer", shell=True, stdin=subprocess.PIPE, stdout=sys.stdout)
  339         proc.stdin.write(shipper.html_template % substitutions)
  340         proc.stdin.close()
  341         proc.wait()
  342         if "web" in phases:
  343             print("INAGADADAVIDA\n")
  344     # Decorations for gitweb
  345     if "gitolite" in phases and os.path.isdir(".git"):
  346         print("echo '%s' >.git/description" % shipper.summary)
  347         if shipper.gitweb_template and shipper.anonymous_clone and shipper.developer_clone:
  348             print("cat >.git/README.html <<'INAGADADAVIDA'")
  349             print(shipper.gitweb_template % substitutions)
  350             print("INAGADADAVIDA\n")
  351         if "gitolite" in phases:
  352             if shipper.developer_clone and re.search(shipper.gitolite_pattern, shipper.developer_clone):
  353                 print("# %s is recognized as gitolite-controlled" % shipper.developer_clone)
  354                 (credential, repo) = shipper.developer_clone.split(":")
  355                 print("ssh -q %s desc %s '%s'" % (credential, repo, shipper.summary))
  356                 if shipper.gitweb_template:
  357                     print("cat .git/README.html | ssh -q %s readme %s set" % (credential, repo))
  358                 print("")
  359 
  360 #
  361 # Metadata extraction
  362 #
  363 
  364 class Specfile:
  365     def __init__(self, filename):
  366         self.filename = filename
  367         self.savannah = siteprops()
  368         self.sourceforge = siteprops()
  369         self.type = None
  370         if filename.endswith(".spec"):
  371             self.type = "RPM"
  372             self.project = self.extract("Name")
  373             self.version = self.extract("Version")
  374             self.website = self.extract("URL")
  375             self.summary = self.extract("Summary")
  376             self.description = self.rpm_get_multiline("description")
  377             last = ""
  378             state = 0
  379             for line in open(self.filename):
  380                 if state == 0:
  381                     if line.startswith("%changelog"):
  382                         state = 1
  383                         continue
  384                 elif state == 1:
  385                     # Skip first line of entry
  386                     state = 2
  387                     continue
  388                 elif state == 2:
  389                     if not line.strip():
  390                         break
  391                     else:
  392                         if line.strip().startswith("-"):
  393                             line = line.lstrip()[1:]
  394                         last += line
  395             if last:
  396                 self.lastchange = last
  397             else:
  398                 self.lastchange = None
  399         elif filename == "control":
  400             self.type = "deb"
  401             self.project = self.extract("Package")
  402             self.version = self.extract("Version")
  403             if self.version and "-" in self.version:
  404                 self.version = self.version.split("-")[0]
  405             self.website = self.extract("Homepage")
  406             self.summary = self.extract("Description")
  407             fp = open(self.filename)
  408             desc = ""
  409             gather = False
  410             while True:
  411                 line = fp.readline()
  412                 if not line:
  413                     break
  414                 if line.startswith("Description:"):
  415                     gather = True
  416                     # With this line in, the first line of the control-file
  417                     # Description is included in the description.
  418                     #desc = line[12:]
  419                     continue
  420                 elif not line.strip():
  421                     gather = False
  422                 if gather:
  423                     if line == " .\n":
  424                         line = "\n"
  425                     else:
  426                         line = line[1:]
  427                     desc += line
  428             fp.close()
  429             if desc:
  430                 self.description = desc.strip() + "\n"
  431             else:
  432                 self.description = None
  433             self.lastchange = None
  434         # shipper-only headers
  435         self.destinations = self.extract("Destinations")
  436         self.gitolite_pattern = self.extract("Gitolite")
  437         self.tag_template = self.extract("VC-Tag-Template")
  438         self.tag_message = self.extract("VC-Tag-Message")
  439         self.instant_message = self.extract("Release-Message")
  440         self.web_extras = self.extract("Web-Extras")
  441         self.html_target = self.extract("HTML-Target")
  442         self.repository_url = self.extract("Repository-URL")
  443         self.developer_clone = self.extract("Developer-Clone")
  444         self.anonymous_clone = self.extract("Anonymous-Clone")
  445         self.openhub_url = self.extract("OpenHub-URL")
  446         self.irc_channel = self.extract("IRC-Channel")
  447         self.debian_packages = self.extract("Debian-Packages")
  448         self.logo = self.extract("Logo")
  449         self.webdir = self.extract("Web-Directory")
  450         self.validate = self.extract("Validate")
  451         self.project_tags = self.extract("Project-Tags")
  452         self.freshcode_name = self.extract("Freshcode-Name")
  453         self.savannah.name = self.extract("Savannah-Name")
  454         self.sourceforge.name = self.extract("SourceForge-Name")
  455         self.sourceforge.folder = self.extract("SourceForge-Folder")
  456     def extract(self, fld):
  457         "Extract a one-line field, possibly embedded as a magic comment."
  458         if self.type == "RPM":
  459             return Specfile.grep("^#?"+fld+r":\s*(.*)", self.filename)
  460         if self.type == "deb":
  461             fld = fld.replace('-', '[-_]')
  462             return Specfile.grep("^(?:XBS-)?"+fld+": (.*)", self.filename)
  463         croak("Invalid specfile type '%s'" % self.type)
  464         return None
  465     def rpm_get_multiline(self, fieldname):
  466         "Grab everything from leader line to just before the next leader line."
  467         fp = open(self.filename)
  468         data = ""
  469         gather = False
  470         while True:
  471             line = fp.readline()
  472             if not line:
  473                 break
  474             # Pick up fieldnames *without* translation options.
  475             if line.strip() == "%" + fieldname:
  476                 gather = True
  477                 continue
  478             elif line[0] == "%":
  479                 gather = False
  480             if gather:
  481                 data += line
  482         fp.close()
  483         if data:
  484             return data.strip() + "\n"
  485         return None
  486     @staticmethod
  487     def grep(regexp, fp):
  488         "Mine for a specified regexp in a file."
  489         # Note: this blank-strips its output!
  490         fp = open(fp)
  491         try:
  492             while True:
  493                 line = fp.readline()
  494                 if not line:
  495                     return None
  496                 m = re.search(regexp, line)
  497                 if m:
  498                     return m.group(1).strip()
  499         finally:
  500             fp.close()
  501         return None
  502 
  503 #
  504 # Who am I?
  505 #
  506 def whoami_really():
  507     "Ask various programs that keep track of who you are who you are."
  508     # Bazaar version-control system
  509     (bzrerr, bzrout) = getstatusoutput("bzr config email")
  510     if bzrerr == 0 and bzrout:
  511         return bzrout
  512     # Git version-control system
  513     (nameerr, nameout) = getstatusoutput("git config user.name")
  514     (emailerr, emailout) = getstatusoutput("git config user.email")
  515     if nameerr == 0 and nameout and emailerr == 0 and emailout:
  516         return "%s <%s>" % (nameout, emailout)
  517     # Various random configs
  518     for (fn, mine) in (
  519         ("~/.hgrc", r"username\s*=\s*(.*)"),                # Mercurial
  520         ("~/.lynxrc", r"personal_mail_address\s*=\s*(.*)")  # Lynx
  521         ):
  522         fn = os.path.expanduser(fn)
  523         if os.path.exists(fn):
  524             for line in open(fn):
  525                 m = re.search(mine, line)
  526                 if m:
  527                     return m.group(1)
  528     # Out of alternatives
  529     return None
  530 
  531 
  532 #
  533 # Shipping methods for public destinations.
  534 #
  535 
  536 def savannah():
  537     if "payload" in phases:
  538         upload("dl.sv.nongnu.org:/releases/%s/" \
  539                % (shipper.savannah.name or shipper.project),
  540                download_deliverables)
  541 
  542 def sourceforge():
  543     sourceforge_name = shipper.sourceforge.name or shipper.project
  544     sourceforge_id = shipper.sourceforge.id or myuserid
  545     if "payload" in phases:
  546         # See https://sourceforge.net/apps/trac/sourceforge/wiki/Release%20files%20for%20download
  547         destdir = "/home/frs/project/%s" % sourceforge_name
  548         if shipper.sourceforge.folder:
  549             destdir += "/" + shipper.sourceforge.folder
  550         print("rsync -avP -e ssh %s '%s,%s@frs.sourceforge.net:%s'" % (
  551             " ".join(download_deliverables),
  552             sourceforge_id,
  553             sourceforge_name,
  554             destdir))
  555     if "web" in phases:
  556         if shipper.webdir:
  557             websources = shipper.webdir + '/'
  558         else:
  559             websources = " ".join(web_deliverables)
  560         # https://sourceforge.net/apps/trac/sourceforge/wiki/Rsync%20over%20SSH
  561         print("rsync -aiv %s %s,%s@web.sourceforge.net:/home/project-web/%s/htdocs/" % (
  562             websources,
  563             shipper.sourceforge.id or myuserid,
  564             sourceforge_name,
  565             sourceforge_name))
  566 
  567 #
  568 # Main sequence
  569 #
  570 
  571 if sys.hexversion < 0x02070200:
  572     sys.stderr.write("shipper: requires Python 2.7.2 or later.\n")
  573     sys.exit(1)
  574 
  575 try:
  576     #
  577     # Process options
  578     #
  579 
  580     parser = argparse.ArgumentParser(description="A project-shipping tool")
  581     parser.add_argument("-v", "--verbose",
  582                       action="store_true", dest="verbose", default=False,
  583                       help="print progress messages to stdout")
  584     parser.add_argument("-d", "--dump",
  585                       action="store_true", dest="dump", default=False,
  586                       help="dump configuration only, no builds or uploads")
  587     parser.add_argument("-x", "--exclude",
  588                       dest="excluded", default="",
  589                       help="exclude some shipping targets")
  590     parser.add_argument("-w", "--web-only",
  591                       action="store_true", dest="webonly", default=False,
  592                       help="do webspace update only")
  593     parser.add_argument("-p", "--phase",
  594                       dest="phase",
  595                       help="perform a single specifed phase")
  596     parser.add_argument("-n", "--non-user",
  597                       dest="nonuser", default="",
  598                       help="treat specified file as the user profile")
  599     parser.add_argument("-i", "--instant-message",
  600                       action="store_true", dest="instant_message", default=False,
  601                       help="perform instant-message notifications")
  602     parser.add_argument("-g", "--gitlab-igor",
  603                       action="store_true", dest="gitlab_igor", default=False,
  604                       help="Use Igor to ship releases")
  605     parser.add_argument("-N", "--no-check",
  606                       action="store_false", dest="precheck", default=True,
  607                       help="disable repository and validation checks")
  608     parser.add_argument("-S", "--no-stale",
  609                       action="store_false", dest="stalecheck", default=True,
  610                       help="disable NEWS date checking")
  611     (options, args) = parser.parse_known_args()
  612 
  613 
  614     if options.webonly or options.instant_message:
  615         options.precheck = False
  616         options.stalecheck = False
  617 
  618     # Phase computation
  619 
  620     valid_phases = set(["payload", "pagemaking", "web", "gitolite", "tagging", "notify", "broadcast"])
  621     phases = copy.copy(valid_phases)
  622     # Undocumented override for use in scripts
  623     if os.getenv("STALECHECK"):
  624         options.stalecheck = os.getenv("STALECHECK") != "no"
  625     if os.getenv("SHIPPER_PHASES"):
  626         options.phase = os.getenv("SHIPPER_PHASES")
  627     if options.phase:
  628         phases = set([])
  629         for phase in options.phase.split(","):
  630             if phase in valid_phases:
  631                 phases.add(phase)
  632             else:
  633                 sys.stderr.write("shipper: invalid phase %s.\n" % phase)
  634                 raise SystemExit(1)
  635     elif options.webonly:
  636         phases = set(["web", "pagemaking", "gitolite"])
  637 
  638     shipper = Shipper()
  639 
  640     #
  641     # Extract metadata and compute control information
  642     #
  643 
  644     # Security check, don't let an attacker elevate privileges
  645     def securecheck(fn):
  646         if stat.S_IMODE(os.stat(fn).st_mode) & 0o0002:
  647             croak("%s must not be world-writeable!" % fn)
  648 
  649     if options.nonuser:
  650         shipper.incorporate(options.nonuser)
  651     else:
  652         # Read in user's profiles
  653         home_profile = os.path.join(os.getenv('HOME'), ".shipper")
  654         if os.path.exists(home_profile):
  655             securecheck(home_profile)
  656             shipper.incorporate(home_profile)
  657         elif options.verbose:
  658             print("shipper: No home_profile (%s); continuing" % home_profile)
  659 
  660         home_profile = os.path.join(os.getenv('HOME'), ".config/shipper")
  661         if os.path.exists(home_profile):
  662             securecheck(home_profile)
  663             shipper.incorporate(home_profile)
  664         elif options.verbose:
  665             print("shipper: No home_profile (%s); continuing" % home_profile)
  666         # User's identity
  667         if not shipper.whoami:
  668             shipper.whoami = whoami_really()
  669         if not shipper.whoami:
  670             croak("please set whoami in your ~/.config.shipper or ~/.shipper file.")
  671 
  672     # Read in per-project metadata
  673     if os.path.exists("control"):
  674         metadata = Specfile("control")
  675     else:
  676         specfiles = glob.glob("*.spec")
  677         if len(specfiles) == 1:
  678             metadata = Specfile(specfiles[0])
  679         else:
  680             croak("must be exactly one RPM specfile in the directory!")
  681 
  682     # Merge in information from the project metadata files
  683     metadata_keys = list(metadata.__dict__.keys())
  684     metadata_keys.remove("destinations")
  685     metadata_keys.remove("filename")
  686     metadata_keys.remove("type")
  687     for name in metadata_keys:
  688         if not getattr(shipper, name):
  689             if name not in Shipper.hardwired:
  690                 setattr(shipper, name, getattr(metadata, name))
  691             else:
  692                 # Copy only metadata values that are not None
  693                 for (k, v) in getattr(metadata, name).__dict__:
  694                     if v:
  695                         setattr(getattr(shipper, name), k, v)
  696 
  697     # Specfiles may set their own destinations
  698     project_destinations = metadata.extract("Destinations")
  699     if project_destinations:
  700         project_destinations = project_destinations.split(",")
  701         shipper.merge_with_negations([x.strip() for x in project_destinations])
  702 
  703     # Shipper-specific project profile
  704     securecheck(".")
  705     here_profile = ".shipper"
  706     if os.path.exists(here_profile):
  707         securecheck(here_profile)
  708         shipper.incorporate(here_profile)
  709 
  710     # Some data can be set in a git config
  711     if os.path.isdir(".git"):
  712         for attr in Shipper.perproject:
  713             (err, out) = getstatusoutput("git config shipper.%s" % attr.replace("_", ""))
  714             if err == 0 and out:
  715                 setattr(shipper, attr, out)
  716         (err, out) = getstatusoutput("git config shipper.destinations")
  717         if err == 0 and out:
  718             shipper.merge_with_negations([x.strip() for x in out.split(",")])
  719 
  720     # Arguments can be variable settings
  721     for arg in args:
  722         if arg.count("=") != 1:
  723             croak("'%s' is not in name=value form." % arg)
  724         else:
  725             (name, val) = arg.split("=")
  726             try:
  727                 setattr(shipper, name, val)
  728             except (SyntaxError, NameError, ValueError):
  729                 croak("ill-formed variable setting at %s" % arg)
  730 
  731     if options.precheck:
  732         # Bail out if we're looking at a repo with local changes
  733         for (vcs, dotdir, cmd) in (
  734             ("git", ".git", "git status | grep -q 'modified'"),
  735             ("svn", ".svn", "svnversion | grep -q 'M'"),
  736             ("hg", ".hg", "hg status -duram | grep -q '^M'"),
  737             ):
  738             if os.path.exists(dotdir):
  739                 if os.system(cmd) == 0:
  740                     croak("can't ship, the %s repository has uncommitted changes" % vcs)
  741         # At this point we have enough marbles to do validation
  742         if shipper.validate:
  743             status = os.system("(%s) 1>&2" % shipper.validate)
  744             if status != 0:
  745                 croak("validation failed with status %d" % status)
  746 
  747     # Apply command-line destination exclusions
  748     if options.excluded:
  749         for excludee in options.excluded.split(","):
  750             if excludee in shipper.destinations:
  751                 shipper.destinations.remove(excludee)
  752             else:
  753                 croak("%s isn't in the destinations!\n" % excludee)
  754 
  755     if not options.dump:
  756         if not shipper.whoami:
  757             # This guard can only be reached with -n on
  758             shipper.whoami = "J. Random Luser <jrl@fubar.net>"
  759 
  760     # Various derived things and defaults
  761     (myrealname, myaddress) = email.utils.parseaddr(shipper.whoami)
  762     (myuserid, myhost) = myaddress.split("@")
  763     if not shipper.freshcode_name:
  764         shipper.freshcode_name = shipper.project
  765     if shipper.sourceforge.name:
  766         if "@" in shipper.sourceforge.name:
  767             (shipper.sourceforge.id, shipper.sourceforge.name) = shipper.sourceforge.name.split("@")
  768 
  769     if options.verbose:
  770         print("shipper: variable extraction finished")
  771 
  772     # Finally, derive the lastchange entry; we'll need it for
  773     # email and other notifications.
  774     shipper.lastchange = None
  775     for filename in ("NEWS", "NEWS.adoc", "HISTORY", "ChangeLog"):
  776         if not shipper.lastchange and os.path.exists(filename):
  777             if options.verbose:
  778                 print("shipper: I see a %s file" % filename)
  779             state = 0
  780             for line in open(filename, "r"):
  781                 if state == 0:       # Skipping file header
  782                     if line[0].isdigit():   # There is no header
  783                         header = line
  784                         state = 1
  785                     elif line.startswith(" ") or line.startswith("\t") or line.startswith("="):
  786                         continue
  787                     elif not line.strip():
  788                         continue
  789                     else:
  790                         header = line
  791                         state = 1
  792                 elif state == 1:     # At release-stanza header
  793                     # Skip first line in the log entry.
  794                     if not shipper.version:
  795                         m = re.match("[0-9.]+", header)
  796                         if m:
  797                             newsversion = m.group(0)
  798                             if shipper.version is not None and shipper.version != newsversion:
  799                                 croak("news version mismatch")
  800                             shipper.version = newsversion
  801                     # Sanity check the date
  802                     if options.stalecheck:
  803                         m = re.search("[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]", header)
  804                         if m is None:
  805                             croak("no date in release-stanza header %r" % header)
  806                         else:
  807                             ymd = time.strftime("%Y-%m-%d", time.localtime())
  808                             if ymd not in header:
  809                                 croak("date looks stale in header %r" % header)
  810                     shipper.lastchange = line
  811                     state = 2
  812                 elif state == 2:     # Past release-stanza header
  813                     if not line.strip():
  814                         break
  815                     else:
  816                         shipper.lastchange += line
  817     if not shipper.lastchange and metadata.lastchange:
  818         shipper.lastchange = metadata.lastchange
  819     if shipper.lastchange:
  820         shipper.lastchange = cgi.escape(shipper.lastchange)
  821     if not shipper.lastchange:
  822         for site in newssites:
  823             if 'site' in shipper.destinations:
  824                 croak("%s notification requires a NEWS, HISTORY or ChangeLog file." % site.capitalize())
  825             elif [x for x in shipper.destinations if "@" in x]:
  826                 croak("E-mail notification requires a NEWS, HISTORY or ChangeLog file.")
  827     # Version check has to wait this long because it could have been mined
  828     # from the project NEWS file.
  829     if not options.dump:
  830         if shipper.version is None:
  831             croak("can't get project version; try e.g. `version=1.2.3` on the command line")
  832         elif shipper.version[0] not in "0123456789":
  833             croak("project version %s appears garbled" % shipper.version)
  834 
  835     # Some destinations imply website locations
  836     if not shipper.website:
  837         if "sourceforge" in shipper.destinations:
  838             shipper.website = "http://%s.sourceforge.net/" % (shipper.sourceforge.name or shipper.project)
  839         # This doesn't work.  Savannah's webspace access is too painful.
  840         #if "savannah" in shipper.destinations:
  841         #    shipper.website = "http://www.nongnu.org/%s/" % shipper.savannah.name
  842     # Download directory has to be computed differently at
  843     # special destinations.
  844     if shipper.website:
  845         if "savannah" in shipper.website:
  846             download = "http://download.savannah.nongnu.org/releases/%s/"+(shipper.savannah.name or shipper.project)
  847         elif "sourceforge" in shipper.website:
  848             download = "http://sourceforge.net/projects/%s/files/" % (
  849                 (shipper.sourceforge.name or shipper.project))
  850             if shipper.sourceforge.folder:
  851                 download += shipper.sourceforge.folder + '/'
  852         else:
  853             download = shipper.website
  854 
  855     #
  856     # Now compute the names of deliverables
  857     #
  858     def versioned(fn):
  859         "Does the specified filename contain a version number?"
  860         return re.search("[0-9]", fn)
  861 
  862     if options.verbose:
  863         print("shipper: starting deliverables computation")
  864 
  865     deliverable_types = (
  866         (re.compile("^README$"),
  867          "roadmap file",
  868          None,
  869          False),
  870         (re.compile("^READ.ME$"),
  871          "roadmap file",
  872          None,
  873          False),
  874         (re.compile("^README.adoc$"),
  875          "roadmap file",
  876          None,
  877          False),
  878         (re.compile("^ChangeLog$"),
  879          "change log",
  880          "ChangeLog",
  881          False),
  882         (re.compile("^NEWS$"),
  883          "project news",
  884          None,
  885          False),
  886         (re.compile("^NEWS.adoc$"),
  887          "project news",
  888          None,
  889          False),
  890         (re.compile("^HISTORY$"),
  891          "project history",
  892          None,
  893          False),
  894         (re.compile("^BUGS$"),
  895          "known bugs",
  896          None,
  897          False),
  898         (re.compile("^BUGS.adoc$"),
  899          "known bugs",
  900          None,
  901          False),
  902         (re.compile("^TODO$"),
  903          "to-do file",
  904          None,
  905          False),
  906         (re.compile("^TODO.adoc$"),
  907          "to-do file",
  908          None,
  909          False),
  910         (re.compile("^AUTHORS$"),
  911          "credits",
  912          None,
  913          False),
  914         (re.compile("^AUTHORS.adoc$"),
  915          "credits",
  916          None,
  917          False),
  918         (re.compile("^COPYING$"),
  919          "project license",
  920          None,
  921          False),
  922         (re.compile("^COPYING.adoc$"),
  923          "project license",
  924          None,
  925          False),
  926         (re.compile(".*-" + str(shipper.version) + r".(tar.gz|tgz)$"),
  927          "gzipped source tarball",
  928          "Tar/GZ",
  929          True),
  930         (re.compile(shipper.project + ".*-" + str(shipper.version) + r".tar.bz2$"),
  931          "bzipped source tarball",
  932          "Tar/BZ",
  933          True),
  934         (re.compile(shipper.project + ".*-" + str(shipper.version) + r".tar.xz$"),
  935          "compressed source tarball",
  936          "Tar/XZ",
  937          True),
  938         (re.compile(shipper.project + ".*-" + str(shipper.version) + r".md5$"),
  939          "source tarball MD5 checksum",
  940          "Checksum",
  941          True),
  942         (re.compile(shipper.project + ".*-" + str(shipper.version) + r".sha224$"),
  943          "source tarball SHA224 checksum",
  944          "Checksum",
  945          True),
  946         (re.compile(shipper.project + ".*-" + str(shipper.version) + r".sha256$"),
  947          "source tarball SHA256 checksum",
  948          "Checksum",
  949          True),
  950         (re.compile(shipper.project + ".*-" + str(shipper.version) + r".sha384$"),
  951          "source tarball SHA384 checksum",
  952          "Checksum",
  953          True),
  954         (re.compile(shipper.project + ".*-" + str(shipper.version) + r".sha512$"),
  955          "source tarball SHA512 checksum",
  956          "Checksum",
  957          True),
  958         (re.compile(shipper.project + ".*-" + str(shipper.version) + r".zip$"),
  959          "zip archive",
  960          "Zip",
  961          True),
  962         (re.compile(shipper.project + ".*-" + str(shipper.version) + r"-.*\.src.rpm$"),
  963          "source RPM",
  964          "SRPM-Package",
  965          True),
  966         (re.compile(shipper.project + ".*-" + str(shipper.version) + r"-.*\.rpm$"),
  967          "binary RPM",
  968          "RPM-Package",
  969          True),
  970         (re.compile(shipper.project + ".*-" + str(shipper.version) + r"-.*\.deb$"),
  971          "Debian package",
  972          "Debian-Package",
  973          True),
  974         )
  975     deliverables = []
  976     for filename in os.listdir("."):
  977         for (regexp, explanation, tag, bulky) in deliverable_types:
  978             if regexp.search(filename):
  979                 # If we're using Igor to create a release on GitLab,
  980                 # don't copy tarballs to where the project resource page is
  981                 # hosted. Instead, Igor is going to cut a Gitlab release at a
  982                 # later stage.
  983                 if shipper.igor_payload and options.gitlab_igor and "tarball" in filename:
  984                     continue
  985                 if not bulky:
  986                     with open(filename) as fp:
  987                         if re.search("shipper: " "ignore this", fp.read()):
  988                             continue
  989                 deliverables.append((filename, explanation, tag, bulky))
  990     if options.verbose:
  991         print("shipper: deliverables: " + " ".join([x[0] for x in deliverables]))
  992 
  993     #
  994     # Might be time to dump
  995     #
  996     if options.dump:
  997         shipper.dump()
  998         raise SystemExit(0)
  999 
 1000     # Special case: We're being called by a script generated by
 1001     # another instance of shipper to perform instant messaging.
 1002     if options.instant_message:
 1003         if tweet_capable:
 1004             twitter = Twython(
 1005                 shipper.twitter_consumer_key,
 1006                 shipper.twitter_consumer_secret,
 1007                 shipper.twitter_access_token,
 1008                     shipper.twitter_access_token_secret
 1009             )
 1010             msg = shipper.instant_message % shipper.__dict__
 1011             try:
 1012                 twitter.update_status(status="shipper says: "+msg)
 1013             except TwythonError as e:
 1014                 print(e)
 1015         sys.stdout.flush()
 1016         raise SystemExit(0)
 1017 
 1018     # Sanity checks
 1019     if not shipper.destinations:
 1020         croak("the Destinations list is empty; nothing to do.")
 1021     if "payload" in phases and not [f_e_t_b[0] for f_e_t_b in deliverables if versioned(f_e_t_b[0])]:
 1022         sys.stderr.write("shipper: warning, no deliverables with versions.\n")
 1023 
 1024     if options.verbose:
 1025         print("shipper: destinations: " + ", ".join(shipper.destinations))
 1026         print("shipper: sanity checks passed")
 1027 
 1028     if not shipper.webdir:
 1029         special_docs = {
 1030             "README.html": "roadmap file",
 1031             "NEWS.html": "project news",
 1032             "TODO.html": "to-do list",
 1033             }
 1034         # Things made with asciidoc should be explained as documentation.
 1035         # We put their rendered HTML on the deliverables list now.
 1036         for filename in glob.glob('*.adoc'):
 1037             rendered = filename.replace(".adoc", ".html")
 1038             explanation = special_docs.get(rendered, "Documentation")
 1039             deliverables.append((rendered, explanation, None, False))
 1040 
 1041         # Compute web-related deliverables, we need this even if not rebuilding
 1042         # the templated page. Includes anything with an HTML, Javascript, or CSS
 1043         # extension.
 1044         for filename in glob.glob('*.html')+glob.glob('*.xhtml'):
 1045             if filename == shipper.html_target:
 1046                 continue
 1047             # Don't gather HTML generated from asciidoc twice
 1048             if os.path.exists(filename.replace(".html", ".adoc")):
 1049                 continue
 1050             with open(filename, "rb") as fp:
 1051                 if re.search(b"shipper: " b"ignore this", fp.read()):
 1052                     continue
 1053             stem = filename[:-4]
 1054             for ext in ("man", "1", "2", "3", "4", "5", "6", "7", "8", "9", "xml"):
 1055                 if os.path.exists(stem + ext):
 1056                     explanation = "HTML rendering of " + stem + ext
 1057                     break
 1058             else:
 1059                 # If the HTML has a <title> element, use it.
 1060                 m = re.search("<title>([^<]*)</title>", open(filename).read())
 1061                 if m:
 1062                     explanation = m.group(1)
 1063                 else:
 1064                     explanation = "HTML page."
 1065             deliverables.append((filename, explanation, None, False))
 1066         if shipper.web_extras is not None:
 1067             for fn in shipper.web_extras.split():
 1068                 firstline = open(fn).readline()
 1069                 if firstline.startswith("#"):
 1070                     explanation = firstline[1:].strip()
 1071                 else:
 1072                     explanation = "Custom web deliverable"
 1073                 deliverables.append((fn, explanation, None, False))
 1074         for untitled in glob.glob('*.js')+glob.glob('*.css'):
 1075             deliverables.append((untitled, None, None, False))
 1076 
 1077         # Template resource pages?
 1078         make_templated_pages(deliverables, download, options)
 1079 
 1080         if "web" in phases and shipper.html_target:
 1081             deliverables.append((shipper.html_target,
 1082                                  "templated web page", None, False))
 1083 
 1084         # We'll want the logo if it exists, too
 1085         if shipper.logo:
 1086             deliverables.append((shipper.logo, "project logo", None, False))
 1087 
 1088     # Compute final deliverables.  This computation needs to coincide
 1089     # with the way web deliverables are distinguished from download
 1090     # deliverables in make_templated_page(), otherwise havoc will ensue.
 1091     all_deliverables = [x[0] for x in deliverables]
 1092     download_deliverables = [x[0] for x in [f_e_s_b for f_e_s_b in deliverables if f_e_s_b[3]]]
 1093     web_deliverables = [x[0] for x in [f_e_s_b for f_e_s_b in deliverables if not f_e_s_b[3]]]
 1094 
 1095     #
 1096     # OK, commands for everything.  First asciidoc formatting
 1097     #
 1098     if "web" in phases:
 1099         asciidoc = "asciidoc"
 1100         if os.path.exists("Makefile") and "asciidoctor" in open("Makefile").read():
 1101             asciidoc = "asciidoctor"
 1102         for filename in glob.glob('*.adoc'):
 1103             print(asciidoc + " " + filename)
 1104 
 1105     # Then the uploads
 1106     for destination in shipper.destinations:
 1107         if destination in ("savannah", "sourceforge"):
 1108             eval(destination + "()")
 1109         elif destination.startswith("ftp:"):
 1110             if "payload" in phases:
 1111                 upload(destination, download_deliverables)
 1112         elif destination.startswith("mailto:"):
 1113             pass    # defer this until a later phase
 1114         elif destination.startswith("irc:"):
 1115             pass    # defer this until a later phase
 1116         elif destination in newssites:
 1117             pass    # defer this until a later phase
 1118         else:
 1119             if "payload" in phases:
 1120                 upload(destination, download_deliverables)
 1121             if "web" in phases:
 1122                 upload(destination, web_deliverables)
 1123 
 1124     # We need a relized tag name ffor both tagging and GitLab release
 1125     tag_name = shipper.tag_template % shipper.__dict__
 1126 
 1127     # Igor can just grab the first tarball it sees
 1128     if "payload" in phases and options.gitlab_igor and shipper.igor_payload():
 1129         for filename in os.listdir("."):
 1130             for (regexp, explanation, tag, bulky) in deliverable_types:
 1131                 if regexp.search(filename) and "tarball" in explanation:
 1132                     print("igor.py 'upload path=%s' 'release name=%s tag=%s notes=/dev/stdin' <<EOF\n%s\nEOF\n" % (filename, tag_name, tag_name, shipper.lastchange))
 1133 
 1134     if "tagging" in phases:
 1135         # Tag the release
 1136         if shipper.tag_template and shipper.tag_message:
 1137             tag_message = shipper.tag_message % shipper.__dict__
 1138             context = shipper.__dict__.copy()
 1139             context.update(locals())
 1140 
 1141             # If we're in the trunk of an SVN repository, we want to tag
 1142             # what just got shipped as an external release.
 1143             if os.path.basename(os.getcwd()) == 'trunk' and os.path.exists(".svn"):
 1144                 print("# This is a Subversion trunk directory...")
 1145                 if os.path.exists("../tags"):
 1146                     print("# I see an svn peer tags directory...")
 1147                     if os.path.exists("../tags/" + tag_name):
 1148                         print("# This release has aleady been tagged.")
 1149                     else:
 1150                         print("# I will copy and tag this release as %s." % tag_name)
 1151                         print("cd .. && svn copy trunk tags/%s && svn -m '%s' commit" % (tag_name, tag_message))
 1152             # FIXME: need hasremote methods for bzr and hg
 1153             for (idir, what, tagger, hasremote, pusher) in (
 1154                 (".git", "git", "git tag -a %(tag_name)s -m '%(tag_message)s'",
 1155                  "git config remote.origin.url", "git push; git push --tags"),
 1156                 (".hg",  "hg",  "hg tag %(tag_name)s -m '%(tag_message)s'",
 1157                  None, "hg push"),
 1158                 (".bzr", "bzr", "bzr tag %(tag_name)s",
 1159                  None, "bzr push"),
 1160                 ):
 1161                 if os.path.exists(idir):
 1162                     print(tagger % context)
 1163                     if hasremote is None or capture(hasremote):
 1164                         print(pusher % context)
 1165 
 1166         if "notify" in phases:
 1167             # Notification to news sites, after uploads and tagging
 1168             for site in newssites:
 1169                 if site in shipper.destinations:
 1170                     if not shipper.website:
 1171                         print("# Can't announce to %s without a primary website!" % site.capitalize())
 1172                     elif not shipper.lastchange:
 1173                         print("# Can't announce to %s without a changes field!" % site.capitalize())
 1174                     else:
 1175                         print("freecode-submit -s %s <<'INAGADADAVIDA'" % site)
 1176                         print("Project: %s" % (shipper.freshcode_name or shipper.project))
 1177                         print("Version: %s" % shipper.version)
 1178                         print("Summary: %s" % shipper.summary)
 1179                         print("Description: %s" % shipper.description.replace("\n", "\n    ").rstrip())
 1180                         if shipper.project_tags:
 1181                             print("Project-Tag-List: %s" % shipper.project_tags)
 1182                         print("Website-URL: %s" % shipper.website)
 1183                         for (f, e, s, b) in deliverables:
 1184                             if s:
 1185                                 if b:
 1186                                     url = download
 1187                                 else:
 1188                                     url = shipper.website
 1189                                 url = os.path.join(url, f)
 1190                                 print("%s-URL: %s" % (s, url))
 1191                         # Avoid bulleted entries, they format badly.
 1192                         changelog = [s.lstrip() for s in shipper.lastchange.split("\n")]
 1193                         sys.stdout.write("\n" + "\n".join(changelog))
 1194                         print("INAGADADAVIDA\n")
 1195 
 1196         if "broadcast" in phases:
 1197             # Email notifications
 1198             maildests = [x[7:] for x in shipper.destinations if x.startswith("mailto:")]
 1199             if maildests:
 1200                 print("sendmail %s <<'INAGADADAVIDA'" % " ".join(maildests))
 1201                 print(shipper.mail_template % shipper.__dict__)
 1202                 print("INAGADADAVIDA\n")
 1203 
 1204             # Ship to IRC channels
 1205             if shipper.irc_channel:
 1206                 irc_destinations = [chan for chan in destination \
 1207                                     if chan.startswith("irc:")]
 1208                 for url in  irc_destinations + shipper.irc_channel.split(","):
 1209                     msg = shipper.instant_message % shipper.__dict__
 1210                     print("irkerd -i '%s' '%s'" % (url, msg))
 1211             # Ship to instant-messaging channels
 1212             if shipper.twitter_consumer_key:
 1213                 print("shipper -i version=%s" % shipper.version)
 1214 
 1215     if phases != set(["pagemaking"]):
 1216         print("# That's all, folks!")
 1217     # Without this we sometime get output truncation when writing to a pipe.
 1218     sys.stdout.flush()
 1219 except KeyboardInterrupt:
 1220     print("# Bye!")
 1221 
 1222 # The following sets edit modes for GNU EMACS
 1223 # Local Variables:
 1224 # mode:python
 1225 # End: