#!/usr/bin/python
#
# prevu -- automated backporter
# Basic usage: prevu <sourcepackagename>
#
# Copyright (C) 2006 John Dong <jdong@ubuntu.com>
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program 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 General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#



import os, sys, glob, shutil

def change_xterm_title(new_title):
  """
  Changes the title on xterm-compatible terminals.
  new_title: The text to change the title to
  """
  if os.getenv('TERM') == 'xterm':
    #Only do it if TERM is xterm...
    print "\033]0;%s\007" % new_title

class NoSuchPackageException(Exception): pass
class NoBuildEnvironment(Exception): pass

class Backport:
  '''Abstract class encapsulating a package to be backported'''
  suffixes={'warty':   '4.10prevu1',
             'hoary':   '5.04prevu1',
             'breezy':  '5.10prevu1',
             'dapper':  '6.06prevu1',
             'edgy':    '6.10prevu1',
             'feisty':  '7.04prevu1',
             'gutsy':   '7.10prevu1',
             'hardy':   '8.04prevu1'}

  def __init__(self):
    raise NotImplementedError
  def check_builder(self):
    if not os.path.exists('/var/cache/prevu/%s.tgz' % self.target_distro): raise NoBuildEnvironment("Not ready to build for %s. You need to run 'DISTRO=%s sudo prevu-init'." % (self.target_distro, self.target_distro))
  def prepare_sources(self):
    '''Finds and unpacks sources'''
    raise NotImplementedError
  def debian_version(self):
    '''Gets the current debian-version of the source package'''
    version=os.popen('cat debian/changelog | head -n 1 | cut -d \( -f 2 | cut -d \) -f 1').read()[:-1]
    if not version:
      raise ValueError("Could not figure out source version. Is debian/changelog corrupted/nonexistent?")
    return version
  def backport_version(self):
    '''Gets the backport version'''
    try:
      if self.debian_version().endswith('~'+self.suffixes[self.target_distro]):
        return self.debian_version()
      else:
        return self.debian_version()+'~'+self.suffixes[self.target_distro]
    except KeyError:
      raise KeyError, "Invalid distribution: %s" % self.target_distro
  def init_working_path(self):
    '''Gets the build directory ready'''
    self.working_path='/var/cache/prevu/src/%s' % (os.getpid())
    while os.path.exists(self.working_path):
      self.working_path+=".1"
    os.mkdir(self.working_path)
  def enter_sourcetree(self):
    '''Enters extracted source package'''
    os.chdir(self.working_path)
    os.mkdir("%s/result" % self.working_path)
    dirs=os.listdir(self.working_path)
    found=False
    for i in dirs:
      if os.path.isdir(i) and not i.endswith('result'):
        os.chdir(i)
        found=True
        break
    if not found:
      raise ValueError("No extracted source tree can be located!")
  def mangle_version(self):
    '''Changes package version to backported package'''
    if self.debian_version() != self.backport_version():
      os.system('dch -v %s -b "Automated backport by prevu. No source changes"' % self.backport_version())
  def do_compile(self):
    '''Invokes the build. Call when source package is unpacked and version number is set'''
    ret=os.system('unset DEBEMAIL; pdebuild --use-pdebuild-internal --buildresult %s/result  -- --basetgz /var/cache/prevu/%s.tgz --buildplace /var/cache/prevu/builds --bindmounts /var/cache/prevu/%s-debs ' % (self.working_path, self.target_distro, self.target_distro))
    if ret != 0:
      raise ValueError("Build failed.")
    else:
      # Build succeeded
      # First, move all the built debs to their final resting site :)
      for pkg in glob.glob(self.working_path+"/result/*.deb"):
        shutil.move(pkg,'/var/cache/prevu/%s-debs' % self.target_distro)
      os.chdir('/var/cache/prevu/%s-debs' % self.target_distro)
      os.system('dpkg-scanpackages . /dev/null > Packages 2>/dev/null')
  def cleanup(self):
    '''Clean up working area'''
    os.system('rm -rf '+self.working_path)
  def backport(self):
    '''Routine for building the backport'''
    self.check_builder()
    try:
      self.init_working_path()
      self.prepare_sources()
      self.enter_sourcetree()
      self.mangle_version()
      self.do_compile()
    finally:
      self.cleanup()

class BackportFromAPT(Backport):
  '''Use APT to get source for backport'''
  def __init__(self,pkgname, target_distro):
    '''
    Class Initializer.
    pkgname: package name to pass to `apt-get source` command
    target_distro: the distribution version to backport to
    '''
    self.pkgname=pkgname
    self.target_distro=target_distro
  def prepare_sources(self):
    os.chdir(self.working_path)
    ret=os.system("/usr/bin/apt-get source %s" % self.pkgname)
    if ret != 0:
      raise ValueError("Fetching source package failed. Are you sure it exists?")

class BackportCurrentDir(Backport):
  '''Build the sources from the current directory'''
  def __init__(self,target_distro):
    self.target_distro=target_distro
  def prepare_sources(self):
    print "Making a temporary copy of current directory..."
    ret=os.system("cp -rp . "+self.working_path+"/source")
    if ret != 0:
      raise ValueError("Could not prepare sources")


class BackportFromDsc(Backport):
  '''Build from a specified .dsc file -- either a local file or a URL'''
  def __init__(self, dsc, target_distro):
    self.dsc=dsc
    self.target_distro=target_distro
  def prepare_sources(self):
    ret=-1
    if self.dsc.startswith("http://") or self.dsc.startswith("ftp://") or self.dsc.startswith("https://"):
      #Is a URL, should be processed with dget
      os.chdir(self.working_path)
      ret=os.system('dget -x %s' % self.dsc)
    else:
      #Assume is a file, and the rest of the source is in current directory
      #Use dpkg-source -x
      ret=os.system('dpkg-source -x -sn %s %s/extracted' % (self.dsc,self.working_path))
    if ret != 0:
      raise ValueError("Extracting source package failed")

class BackportFromLP(BackportFromDsc):
   def __init__(self,arg, target_distro):
      self.arg=arg
      self.target_distro=target_distro
   def prepare_sources(self):
      # Scrape from Launchpad the source package :)
      # Expects: commandline arg in the form pkgname/distro
      import re
      os.chdir(self.working_path)
      args=self.arg.split("/")
      dist=""
      src=""
      if len(args) == 1:
         # Only package specified
         # Default to newist distro
         dist="hardy"
         print "Launchpad Fetcher: No distro version specified, assuming %s" % dist
         src=args[0]
      elif len(args) == 2:
         (src, dist) = args
      else:
         raise ArgumentError, "Launchpad Fetcher: Arguments not understood!"
      contents=os.popen('wget -q https://launchpad.net/ubuntu/%s/+source/%s -O-' % (dist, src)).read()
      links=re.findall('a href=\"(.*\.(dsc|diff\.gz|tar\.gz))\"', contents)
      for i in links:
         os.system("wget http://launchpad.net%s" % i[0])
      if os.system("dpkg-source -x *.dsc") != 0:
         raise ValueError, "Failed to fetch and extract source. Ensure that the package specified is a valid source package name and Launchpad is not down."

      


if __name__=='__main__':
  DIST=os.getenv('DIST') or ""
  if not DIST in Backport.suffixes.keys():
    # Distro got through environment is not valid, building against running distro
    print >> sys.stderr, "I: Building against currently running distro:",
    DIST=os.popen('lsb_release -c').read().split(':')[1].strip()
    print >> sys.stderr, DIST
  else:
    print >> sys.stderr, "I: Building against specified distro:", DIST
  try:
    if len(sys.argv) == 2:
      change_xterm_title('prevu (%s): %s' % (DIST, sys.argv[1]))
      if sys.argv[1].endswith(".dsc"):
        BackportFromDsc(sys.argv[1],DIST).backport()
      elif sys.argv[1].startswith("lp:"):
        BackportFromLP(sys.argv[1][3:],DIST).backport()
      else:
        BackportFromAPT(sys.argv[1],DIST).backport()
    elif len(sys.argv) == 1 and os.path.exists("./debian"):
      change_xterm_title('prevu (%s): %s' % (DIST, os.getcwd()))
      BackportCurrentDir(DIST).backport()
    else:
      print "Usage (fetch from APT): %s source_package_name" % sys.argv[0]
      print "Usage (build current directory): %s" % sys.argv[0]
      print "Usage (build from .dsc file): %s dscfile_or_url" % sys.argv[0]
      print "Usage (fetch from Launchpad): %s lp:source_package_name/distro_name" % sys.argv[0]
      print "Prevu builds for your currently running version of Ubuntu. To override this, use the DIST environment variable"
      print "Build for Warty Warthog: DIST=warty %s" % sys.argv[0]
      sys.exit(1)
    print "** Success!. You can find source packages and .debs at /var/cache/prevu/%s-debs **" % DIST
  except Exception, e:
     print "========================"
     print "Prevu Error: %s" % e
     print "Prevu encountered an error performing your build. The actual"
     print "error message may be further up in the scrollback before pbuilder"
     print "exited. Please look for a failed dependency or compile error in"
     print "the full output."
     sys.exit(2)
