#!/usr/bin/env python

# Twisted, the Framework of Your Internet
# Copyright (C) 2001 Matthew W. Lefkowitz
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

from __future__ import nested_scopes

### Twisted Preamble
# This makes sure that users don't have to set up their environment
# specially in order to run these programs from bin/.
import sys, os, string, time, glob
if string.find(os.path.abspath(sys.argv[0]), os.sep+'Twisted') != -1:
    sys.path.insert(0, os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), os.pardir, os.pardir)))
sys.path.insert(0, os.curdir)
### end of preamble

#
# The Twisted release script. This is terribly unix-centric.
#

from twisted.python import usage, util, failure
from twisted import copyright

import shutil, tempfile, re

# magic for CVS guessing
try:
    defaultRoot = open(util.sibpath(sys.argv[0], 'CVS/Root')).read().strip()
except:
    defaultRoot = None

defaultOptions = '--upver --tag --exp --dist --docs --balls --rel --deb' # --debi'



debug = 0

class Options(usage.Options):
    optParameters = [['release-version', 'V', None,
                      "The version of this release."],

                     ['oldver', 'o', copyright.version,
                      "The previous version to replace with the new version (only relevant for --upver)"],

                     ['release', 'r', os.path.expanduser('~/Releases'),
                      "The directory where your Twisted release archive is (only relevant for --rel)."],

                     ['sfname', 'n', os.environ['USER'], "The Sourceforge.net user name"],

                     ['cvsroot', 'c', defaultRoot, "The CVSROOT to export and/or checkout from. (only revelant for --exp and --checkout)."],

                     ['root', 't', os.path.abspath('Twisted.CVS'), "The directory containing your CVS checkout (unneccessary if using --checkout)."]]

    longdesc = "Specify -v and all steps to execute; if no steps are given, default is %r." % defaultOptions
    
    def __init__(self):
        usage.Options.__init__(self)
        self['commands'] = []

    def opt_checkout(self):
        """Checkout Twisted HEAD to $CWD/$root.
        Reqs: Access to a repository containing Twisted
        """
        self['commands'].append(CheckOut)

    def opt_upver(self):
        self['commands'].append(UpdateVersion)

    def opt_tag(self):
        """`cvs tag -c <version>'s a CVS checkout.
        Reqs: A `Twisted' directory."""
        self['commands'].append(Tag)

    def opt_exp(self):
        """Exports Twisted from cvsroot as given with -c.
        Reqs: release-$ver has been tagged."""
        self['commands'].append(Export)
        
    def opt_dist(self):
        """Copies and prepares a Twisted directory for distribution.
        Reqs: 'Twisted' exists, 'Twisted-$ver' doesn't."""
        self['commands'].append(PrepareDist)

    def opt_docs(self):
        """Generate documentation.
        Reqs: Twisted-$ver exists."""
        self['commands'].append(GenerateDocs)

    def opt_balls(self):
        """Creates tarballs.
        Reqs: Twisted-$ver exists."""
        self['commands'].append(CreateTarballs)

    def opt_rel(self):
        """Copies tarballs to ~/Releases.
        Reqs: Tarballs."""
        self['commands'].append(Release)

    def opt_deb(self):
        """Creates a debian mini-repository in ~/Releases/debian-ver/.
        Reqs: release and access to the twistedmatrix.com unstable chroot (through localhost: this means you need to run the script from pyramid!)."""
        self['commands'].append(MakeDebs)

    def opt_debi(self):
        """Copies a debian mini-repository in ~/Releases/debian-ver/ to /twisted/Debian/.
        Reqs: debs and existence of skeleton in /twisted/Debian"""
        self['commands'].append(InstallDebs)

    def opt_sourceforge(self):
        """Copies everything from /twisted/Releases to sourceforge"""
        self['commands'].append(Sourceforge)

    def opt_upgrade(self):
        """Upgrade Debian packages"""
        self['commands'].append(UpgradeDebian)

    def opt_debug(self):
        """Enable debug-mode: ask before running *every* shell command."""        
        global debug
        debug = 1

    opt_d = opt_debug


class Transaction:
    """I am a dead-simple Transaction."""

    sensitiveUndo = 0

    def run(self, data):
        """Try to run this self.doIt; if it fails, call self.undoIt and return a Failure."""
        try:
            self.doIt(data)
        except:
            f = failure.Failure()
            print "%s failed!" % self.__class__.__name__
            if self.sensitiveUndo:
                if raw_input("Are you sure you want to roll back "
                             "this transaction? ").lower().startswith('n'):
                    return f
            print "rolling back transaction."
            self.undoIt(data, f)
            return f

    def doIt(self, data):
        """Le's get it on!"""
        raise NotImplementedError

    def undoIt(self, data, fail):
        """Oops."""
        raise NotImplementedError
        

#errors

class DirectoryExists(OSError):
    """Some directory exists when it shouldn't."""
    pass

class DirectoryDoesntExist(OSError):
    """Some directory doesn't exist when it should."""
    pass

class CommandFailed(OSError):
    pass


def main():
   
    try:
        opts = Options()
        opts.parseOptions()
    except usage.UsageError, ue:
        print "%s: %s" % (sys.argv[0], ue)
        sys.exit(2)

    if not opts['release-version']:
        print "Please specify a version."
        sys.exit(2)

    defaultCommands = [UpdateVersion, Tag, Export, PrepareDist, GenerateDocs, CreateTarballs, Release, MakeDebs]#, InstallDebs]

    if not opts['commands']:
        opts['commands'] = defaultCommands

    sys.path.insert(0, os.path.abspath('Twisted'))

    print "going to do", [x.__name__ for x in opts['commands']]

    last = None

    for command in opts['commands']:
        try:
            f = command().run(opts)
            if f is not None:
                raise f
        except:
            print "ERROR: %s failed. last successful command was %s. Traceback follows:" % (command.__name__, last)
            import traceback
            traceback.print_exc()
            break
        
        last = command

# utilities


def sh(command, sensitive=0):
    """
    I'll try to execute `command', and if `sensitive' is true, I'll ask before running it.
    If the command returns something other than 0, I'll raise CommandFailed(command).
    """
    if debug or sensitive:
        if raw_input("%r ?? " % command).startswith('n'):
            return
    print command
    if os.system(command) != 0:
        raise CommandFailed(command)

        
def replaceInFile(filename, oldstr, newstr):
    """I replace the text `oldstr' with `newstr' in `filename' using sed and mv."""
    sh('cp %s %s.bak' % (filename, filename))
    sh("sed -e 's/%s/%s/' < %s > %s.new" % (re.escape(oldstr), newstr, filename, filename))
    sh('cp %s.new %s' % (filename,  filename))


##
# The transactions.
##
    
class CheckOut(Transaction):
    # failure modes:
    #  * `root' already exists.
    #
    # state changes:
    #  * `root' is created.
    
    def doIt(self, opts):
        print "CheckOut"
        cvsroot = opts['cvsroot']
        target = opts['root']
        
        if os.path.exists(target):
            raise DirectoryExists("%s (--root) already exists" % target)
        
        tmp = None
        if os.path.exists('Twisted'):
            tmp = tempfile.mktemp()
            sh('mv Twisted %s' % tmp) #hrm...

        
        sh('cvs -z6 -d %s co Twisted' % (cvsroot))
        sh('mv Twisted %s' % target)
        
        if tmp:
            sh('mv %s Twisted' % tmp)

    def undoIt(self, opts, fail):
        if fail.check(DirectoryExists):
            return #we don't want to remove the directory if we didn't create it
        
        if os.path.exists(opts['root']):
            sh('rm -rf %s' % opts['root'])

class UpdateVersion(Transaction):
    # fail on
    #  * ? (a failed `sh' command)
    #
    # state changes:
    #  * the CVS tree is modified. to back out we must copy all the .bak files created by replaceInFile back to their original location, and remove them.
    #  * the tree is committed. this is atomic, afaict. [this isn't, but
    #    pretend it is anyway -- Moshe]

    files = None
    
    def doIt(self, opts):
        print "UpdateVersion"
        oldver = opts['oldver']
        newver = opts['release-version']

        r = opts['root']
        self.files = ('README', 'twisted/copyright.py', 'admin/twisted.spec')
        for file in self.files:
            replaceInFile(os.path.join(r, file), oldver, newver)
        sh('cd %s &&  cvs -q diff || true' % (r))
        sh('cd %s &&  cvs commit -m "Preparing for %s" %s' % (r, newver, ' '.join(self.files)), 1)

    def undoIt(self, opts, fail):
        if self.files:
            for file in self.files:
                try:
                    sh('mv %s.bak %s' % (file, file))
                except:
                    print "WARNING: couldn't move %s.bak back to %s, chugging along" % (file, file)
        


class Tag(Transaction):
    def doIt(self, opts):
        print "Tag"
        ver = opts['release-version']
        sh('cd %s &&  cvs tag -c release-%s' % (opts['root'], ver.replace('.', '_')), 1)

    #tagging is atomic, afaict, so no undoIt


class Export(Transaction):
    # errors? no idea
    def doIt(self, opts):
        print "Export"
        root = opts['cvsroot']
        ver = opts['release-version']
        sh('cvs -d%s export -r release-%s Twisted' % (root, ver.replace('.', '_')))

    def undoIt(self, opts, fail):
        sh('rm -rf Twisted')

class PrepareDist(Transaction):
    def doIt(self, opts):
        print "PrepareDist"
        ver = opts['release-version']
        tdir = "Twisted-%s" % ver

        if os.path.exists(tdir):
            raise DirectoryExists("PrepareDist: %s exists already." % tdir)

        shutil.copytree('Twisted', tdir)

    def undoIt(self, opts, fail):
        #don't delete the directory if we didn't create it!
        if not fail.check(DirectoryExists):
            ver = opts['release-version']
            tdir = "Twisted-%s" % ver
            
            sh('rm -rf %s' % tdir)
        

class GenerateDocs(Transaction):

    # documentation generation can take a looong time,
    #so we don't want to force redoing everything
    sensitiveUndo = 1 
    
    def doIt(self, opts):
        print "GenerateDocs"
        
        ver = opts['release-version']
        tdir = "Twisted-%s" % ver
        
        if not os.path.exists('%s' % tdir):
            raise DirectoryDoesntExist("GenerateDocs: %s doesn't exist!" % tdir)
        
        print "makeDocs: epydoc."
        sh('cd %s &&  ./admin/epyrun -o doc/api' % (tdir))

        print "makeDocs: process-docs."
        sh('cd %s && ./admin/process-docs %s' % (tdir, ver))

        #shwack the crap
        print "copyDist: Stripping *.pyc, .cvsignore"
        for x in ['*.pyc', '.cvsignore']:
            sh('find %s -name "%s" | xargs rm -f' % (tdir, x))


    def undoIt(self, opts, fail):

        if fail.check(DirectoryDoesntExist):
            #no state change here
            return

        ver = opts['release-version']
        tdir = "Twisted-%s" % ver

        #first shwack the epydocs
        try:
            sh('mv %s/doc/api/index.html.bak gendocs.index.html' % tdir)
        except:
            sh('mv %s/doc/api/index.html gendocs.index.html' % tdir)
        sh('rm -rf %s/doc/api/*' % tdir)
        sh('mv gendocs.index.html %s/doc/api/index.html' % tdir)
            
        #then swhack the results of generate-domdocs
        sh('rm -f %s/doc/howto/*.xhtml' % tdir)
        sh('rm -f %s/doc/specifications/*.xhtml' % tdir)

        #then swhack the results of the latex stuff
        sh('cd %s/doc/howto && rm -f *.eps *.tex *.aux *.log book.*' % tdir)

class CreateTarballs(Transaction):

    def doIt(self, opts):
        print "CreateTarballs"

        ver = opts['release-version']
        tdir = "Twisted-%s" % ver

        if not os.path.exists(tdir):
            raise DirectoryDoesntExist("%s doesn't exist" % tdir)
        
        print "CreateTarballs: Twisted_NoDocs."
        sh('''
              tar --exclude %(tdir)s/doc -cf - %(tdir)s |gzip -9 > Twisted_NoDocs-%(ver)s.tar.gz && 
              tar --exclude %(tdir)s/doc -cjf Twisted_NoDocs-%(ver)s.tar.bz2 %(tdir)s
           ''' % locals())

        print "CreateTarballs: Twisted"
        sh('''
              tar cf - %(tdir)s | gzip -9 > %(tdir)s.tar.gz&&
              tar cjf   %(tdir)s.tar.bz2 %(tdir)s 
           ''' % locals())

        print "makeBalls: TwistedDocs"
        docdir = "TwistedDocs-%s" % ver
        sh(  '''
             cd %(tdir)s && 
             mv doc %(docdir)s

             tar cf -  %(docdir)s | gzip -9 > %(docdir)s.tar.gz&& 
             mv %(docdir)s.tar.gz ../ && 

             tar cjf   %(docdir)s.tar.bz2 %(docdir)s && 
             mv %(docdir)s.tar.bz2 ../
             ''' % locals())

    def undoIt(self, opts, fail):
        ver = opts['release-version']
        for ext in ['tar.gz', 'tar.bz2']:
            for prefix in ['Twisted_NoDocs', 'TwistedDocs', 'Twisted']:
                try:
                    sh('rm -f %s-%s.%s' % (prefix, ver, ext))
                except:
                    pass

class Release(Transaction):
    def doIt(self, opts):
        print "Release"
        
        rel = opts['release']
        ver = opts['release-version']
        tdir = "Twisted-%s" % ver

        if not os.path.exists(rel):
            print "Release: Creating", rel
            os.mkdir(rel)

        if not os.path.exists("%s/old" % rel):
            print "Release: creating %s/old" % rel
            os.mkdir("%s/old" % rel)

        print "Release: Moving old releases to %s/old" % rel
        sh('mv %(rel)s/*.tar.gz %(rel)s/*.tar.bz2 %(rel)s/old || true' % locals())

        print "Release: Copying Twisted_NoDocs."
        sh('''
              cp Twisted_NoDocs-%(ver)s.tar.gz  %(rel)s &&
              cp Twisted_NoDocs-%(ver)s.tar.bz2 %(rel)s
             ''' % locals())


        print "Release: Copying TwistedDocs."
        sh('''
              cp TwistedDocs-%(ver)s.tar.gz  %(rel)s &&
              cp TwistedDocs-%(ver)s.tar.bz2 %(rel)s
           ''' % locals())

        print "Release: Copying Twisted."
        sh('''
              cp %(tdir)s.tar.gz %(rel)s &&
              cp %(tdir)s.tar.bz2 %(rel)s
           ''' % locals())

    def undoIt(self, opts, fail):
        ver = opts['release-version']
        rel = opts['release']

        for ext in ['zip', 'tar.gz', 'tar.bz2']:
            for prefix in ['Twisted_NoDocs', 'TwistedDocs', 'Twisted']:
                try:
                    sh('rm -f %s/%s-%s.%s' % (rel, prefix, ver, ext))
                except:
                    pass


class MakeDebs(Transaction):

    #takes a while
    sensitiveUndo = 1
    
    def doIt(self, opts):
        print "MakeDebs"
        rel = opts['release']
        ver = opts['release-version']
        tgz = os.path.join(rel, 'Twisted-%s.tar.gz' % ver)
        unique = '%s.%s' % (time.time(), os.getpid())
        os.mkdir('/sid-chroot/tmp/%s' % unique)
        sh('cd /sid-chroot/tmp/%s && tar xzf %s' % (unique, tgz))
        sh("ssh -p 9022 localhost "
                  "'cd /tmp/%(unique)s && ./Twisted-%(ver)s/admin/make-deb -a'"%vars())
        if not os.path.isdir(os.path.join(rel, 'debian-%s' % ver)):
            os.mkdir(os.path.join(rel, 'debian-%s' % ver))
        sys.stdout.write("Moving files to %s" % os.path.join(rel, 'debian-%s'%ver))
        sys.stdout.flush()
        for file in glob.glob('/sid-chroot/tmp/%s/*' % unique):
            if not os.path.isfile(file):
                continue
            sh('cp %(file)s %(rel)s/debian-%(ver)s' % vars())
            sys.stdout.write(".")
            sys.stdout.flush()
        sys.stdout.write("\n")
        os.chdir("%(rel)s/debian-%(ver)s" % vars())
        sh('tar xzf %(tgz)s Twisted-%(ver)s/admin' % vars())
        sh('mv Twisted-%(ver)s/admin/override .' % vars())
        sh('./Twisted-%(ver)s/admin/createpackages override'% vars())
        sh('rm -rf woody')
        os.mkdir('woody')
        sh('cp *.orig.tar.gz *.diff.gz *.dsc woody/')
        os.chdir('woody')
        sh('dpkg-source -x *.dsc')
        twisted_dir = filter(os.path.isdir, glob.glob('twisted-*'))[0]
        os.chdir(twisted_dir)
        replaceInFile('debian/changelog', ver+'-1', ver+'-1woody')
        replaceInFile('debian/control', ', python2.3-dev', '')
        sh('rm -f debian/*.bak')
        sh('dpkg-buildpackage -rfakeroot -us -uc')
        os.chdir('..')
        sh('rm -rf %(twisted_dir)s' % vars())
        sh('../Twisted-%(ver)s/admin/createpackages ../override'% vars())

        sh('rm -rf ../Twisted-%(ver)s' % vars())
        sh('rm -rf /sid-chroot/tmp/%s' % unique)

    def undoIt(self, opts, fail):
        rel = opts['release']
        ver = opts['release-version']
        sh("rm -rf %(rel)s/debian-%(ver)s" % vars())
    

class InstallDebs(Transaction):

    target = '/twisted/Debian'

    def doIt(self, opts):
        rel = opts['release']
        ver = opts['release-version']
        target = self.target
        for file in ('Packages.gz', 'Sources.gz', 'override'):
            if os.path.isfile('%(target)s/%(file)s' % vars()):
                sh('rm -f %(target)s/%(file)s.old' % vars())
                sh('mv %(target)s/%(file)s %(target)s/%(file)s.old' % vars())
            if os.path.isfile('%(target)s/woody/%(file)s' % vars()):
                sh('rm -f %(target)s/woody/%(file)s.old' % vars())
                sh('mv %(target)s/woody/%(file)s %(target)s/woody/%(file)s.old' % vars())
        for file in os.listdir('%(rel)s/debian-%(ver)s/' % vars()):
            if file == 'woody':
                continue
            sh("cp %(rel)s/debian-%(ver)s/%(file)s %(target)s/" % vars())
        sh("cp %(rel)s/debian-%(ver)s/woody/* %(target)s/woody/" % vars())

    def undoIt(self, opts, fail):
        ver = opts['release-version']
        target = self.target
        for dir in (target, target+'/woody'):
            for file in glob.glob('%(dir)s/*.old' % vars()):
                new = os.path.splitext(file)[0]
                sh('mv %(file)s %(new)s' % vars())
                sh('mv %(file)s %(new)s' % vars())
            sh('rm -f %(dir)s/*%(ver)s*' % vars())


class Sourceforge(Transaction):

    #takes a long time
    sensitiveUndo = 1
    
    def doIt(self, opts):
        name = opts['sfname']
        rel = opts['release']
        ver = opts['release-version']
        path = '/home/users/'+name[0]+'/'+name[:2]+'/'+name 
        sh("ssh %(name)s@shell.sf.net mkdir Twisted-%(ver)s || true" % vars())
        sh("scp -r %(rel)s/Twisted*%(ver)s* %(rel)s/debian-%(ver)s "
           "%(name)s@shell.sf.net:%(path)s/Twisted-%(ver)s/" % vars())
        sh("echo "
           "'"
           "umask 0002&&"
           "rm -f /home/groups/t/tw/twisted/htdocs/debian/woody/Packages.gz&&"
           "rm -f /home/groups/t/tw/twisted/htdocs/debian/woody/Sources.gz&&"
           "rm -f /home/groups/t/tw/twisted/htdocs/debian/woody/override&&"
           "rm -f /home/groups/t/tw/twisted/htdocs/debian/Packages.gz&&"
           "rm -f /home/groups/t/tw/twisted/htdocs/debian/Sources.gz&&"
           "rm -f /home/groups/t/tw/twisted/htdocs/debian/override&&"
           "mv Twisted-%(ver)s/debian-%(ver)s/woody/* "
              "/home/groups/t/tw/twisted/htdocs/debian/woody/&&"
           "rmdir Twisted-%(ver)s/debian-%(ver)s/woody&&"
           "mv Twisted-%(ver)s/debian-%(ver)s/* "
              "/home/groups/t/tw/twisted/htdocs/debian/&&"
           "rmdir Twisted-%(ver)s/debian-%(ver)s&&"
           "mv Twisted-%(ver)s/* /home/groups/t/tw/twisted/htdocs&&"
           "cd /home/groups/t/tw/twisted/htdocs&&"
           "tar xzf TwistedDocs-%(ver)s.tar.gz'"
           "|ssh %(name)s@shell.sf.net newgrp twisted" % vars())


class UpgradeDebian(Transaction):

    def doIt(self, opts):
        rel = opts['release']
        ver = opts['release-version']
        path = "%(rel)s/debian-%(ver)s/woody/" % vars()
        sh("sudo dpkg -i %(path)s/*.deb" % vars())


if __name__=='__main__':
    main()

