#! /usr/bin/python
#
# copyright (c) 2006 Josselin Mouette <joss@debian.org>
# Licensed under the GNU Lesser General Public License, version 2.1
# See COPYING for details

import sys,os,os.path
from optparse import OptionParser
from py_compile import compile, PyCompileError

basepath='/var/lib/python-support'
sourcepath='/usr/share/python-support'
extensionpath='/usr/lib/python-support'

parser = OptionParser(usage="usage: %prog [-v] [-c] package_directory [...]\n"+
			    "       %prog [-v] [-c] package.dirs [...]\n"+
			    "       %prog [-v] [-a|-f]")

parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
                  help="verbose output", default=False)
parser.add_option("-c", "--clean", action="store_true", dest="clean_mode",
		  help="clean modules instead of compiling them",
		  default=False)
parser.add_option("-a", "--rebuild-all", action="store_true",
                  dest="rebuild_all", default=False,
		  help="rebuild all private modules for a new default python version")
parser.add_option("-f", "--force-rebuild-all", action="store_true",
                  dest="rebuild_everything", default=False,
		  help="rebuild all modules, including public modules for all python versions.")
parser.add_option("-b", "--bytecompile", action="store_true", dest="force_private",
                  help="[deprecated] byte-compilation mode: only handle private modules",
                  default=False)
parser.add_option("-i", "--install", action="store_true", dest="force_public",
                  help="[deprecated] installation mode: only handle public modules",
		  default=False)

(options, args) = parser.parse_args()

if not os.path.isdir(basepath):
  os.mkdir(basepath)

sys.path.append("/usr/lib/python-support/private/")
import pysupport
from pysupport import py_supported,py_installed,py_oldversions

def debug(x):
  if(options.verbose):
    print x

# I should use the sets type instead
def isect(l1,l2):
  return [i for i in l1 if i in l2]
  
def concat(l1,l2):
  return l1 + [i for i in l2 if i not in l1]

versions_dict={}

def dir_versions(dir):
  if dir not in versions_dict:
    verfile=os.path.join(dir,'.version')
    if dir.startswith(extensionpath):
      # Directory in /usr/lib: only one version
      vers=os.path.split(dir)[1]
      if vers in py_supported:
        versions_dict[dir]=[vers]
      else:
        versions_dict[dir]=[]
    elif dir.startswith(sourcepath):
      # Directory in /usr/share
      extdir=dir.replace(sourcepath,extensionpath,1)
      if os.path.exists(verfile):
        # If we have a .version, use it
        versions_dict[dir]=pysupport.version_list(file(verfile).readline())
      elif os.path.isdir(extdir):
        # Try to obtain the list of supported versions
        # from the extensions in /usr/lib
        versions_dict[dir]=isect(py_supported,os.listdir(extdir))
      else:
        # Otherwise, support all versions
        versions_dict[dir]=py_supported
    else:
      raise "[Internal error] %s: unsupported path for byte-compilation."
  return versions_dict[dir]

def bytecompile_only(basedir,dir,file):
  if file.endswith('.py'):
    fullpath=os.path.join(basedir,dir,file)
    debug("compile "+fullpath+'c')
    try:
      # Note that compile doesn't raise PyCompileError by default
      compile(fullpath, doraise=True)
    except IOError, (errno, strerror):
      sys.stderr.write("WARNING: I/O error while trying to byte-compile %s (%s): %s\n" % (fullpath, errno, strerror))
    except PyCompileError, inst:
      sys.stderr.write("WARNING: compile error while trying to byte-compile %s: %s\n" % (fullpath, inst.msg))
    except:
      sys.stderr.write("WARNING: unexpected error while trying to byte-compile %s: %s\n" % (fullpath, sys.exc_info()[0]))

def clean_simple(basedir,dir,file):
  if file.endswith('.py'):
    for ext in ['c','o']:
      fullpath=os.path.join(basedir,dir,file+ext)
      if os.path.exists(fullpath):
        debug("remove "+fullpath)
        os.remove(fullpath)

def install_modules(versions):
  def install_modules_func(basedir,dir,file):
    if file == '.version':
      return
    fullpath=os.path.join(basedir,dir,file)
    for py in isect(dir_versions(basedir),versions):
      destpath=os.path.join(basepath,py,dir,file)
      try:
        os.makedirs(os.path.join(basepath,py,dir))
      except OSError:
        pass
      if file[-4:] not in ['.pyc','.pyo']:
        debug("link "+destpath)
        # os.path.exists returns False for broken symbolic links
        if os.path.exists(destpath) or os.path.islink(destpath):
          # Oops, the file already exists. 
          # Check whether we are conflicting with something else.
          for otherdir in dirs_i:
            if otherdir == basedir:
              continue
            if os.path.exists(os.path.join(otherdir,dir,file)):
              raise "Trying to overwrite %s which is already provided by %s"%(os.path.join(dir,file),otherdir)
          # This is probably a case of postinst re-running. 
          # Let's proceed.
          debug("overwrite! "+destpath)
          os.remove(destpath)
        os.symlink(fullpath,destpath)
      # Files are NOT byte-compiled here, this MUST be done later.
  return install_modules_func

def clean_modules(basedir,dir,file):
  fullpath=os.path.join(basedir,dir,file)
  for py in dir_versions(basedir):
    destpath=os.path.join(basepath,py,dir,file)
    l=[destpath]
    if file.endswith('.py'):
      l+=[destpath+'c',destpath+'o']
    for path in l:
      if os.path.exists(path):
        debug("remove "+path)
        os.remove(path)
    try:
      os.removedirs(os.path.join(basepath,py,dir))
    except OSError:
      pass

def clean_modules_gen(versions):
  return clean_modules

def process(basedir,func):
  debug("Looking at %s..."%(basedir))
  for dir, dirs, files in os.walk(basedir):
    dir = dir[len(basedir):].lstrip('/')
    for file in files:
      func(basedir, dir, file)
    for file in dirs:
      if os.path.islink(os.path.join(basedir,dir,file)):
        func(basedir, dir, file)

def process_extensions(basedir,func,version=None):
  basedir=basedir.replace(sourcepath,extensionpath,1)
  if os.path.isdir(basedir):
    for vers in os.listdir(basedir):
      if version and vers != version:
        continue
      verdir=os.path.join(basedir,vers)
      if os.path.isdir(verdir):
        process(verdir,func([vers]))

def dirlist_file(f):
  return [ l.rstrip('\n') for l in file(f) if len(l)>1 ]

def generate_pathfile(py):
  path=os.path.join(basepath,py)
  if not os.path.isdir(path):
    return
  pathfile=os.path.join(path,".path")
  debug("Generation of %s..."%pathfile)
  pathlist=[path]
  for f in os.listdir(path):
    f=os.path.join(path,f)
    if f.endswith(".pth") and os.path.isfile(f):
      for l in file(f):
        l=l.rstrip('\n')
        pathlist.append(l)
        pathlist.append(os.path.join(path,l))
  fd=file(pathfile,"w")
  fd.writelines([l+'\n' for l in pathlist])

def bytecompile_all(py,path=None):
  if not path:
    path=os.path.join(basepath,py)
    generate_pathfile(py)
  if not os.path.isdir(path):
    return
  debug("Byte-compilation of whole %s..."%path)
  os.spawnl(os.P_WAIT, '/usr/bin/'+py, py,
            os.path.join('/usr/lib/',py,'compileall.py'), '-q', path)

def bytecompile_privatedir(basedir):
  versionfile=os.path.join(basedir,".pyversion")
  if os.path.isfile(versionfile):
    specific_version=file(versionfile).readline().rstrip('\n')
    bytecompile_all("python"+specific_version,basedir)
  else:
    process(basedir,bytecompile_only)

# Parse arguments
do_dirs_i=[]
do_dirs_b=[]
for arg in args:
  if os.path.isabs(arg):
    if not arg.startswith(sourcepath):
      parser.error("%s is not in the python-support directory."%arg)
  else:
    arg=os.path.join(sourcepath,arg)
  if not os.path.exists(arg):
    parser.error("%s does not exist"%arg)
  if arg.endswith('.dirs'):
    do_dirs_b+=dirlist_file(arg)
    if options.force_public:
      parser.error("Option -i cannot be used with a private module .dirs file.")
  elif os.path.isdir(arg):
    do_dirs_i.append(arg)
    if options.force_private:
      parser.error("Option -b cannot be used with a public module directory.")
  else:
    parser.error("%s is not a directory"%arg)


# Read full list from the source directory
# directories are stuff to be installed
# foo.dirs files list directories to bytecompile in place
dirs_b = []
dirs_i = []
for f in os.listdir(sourcepath):
  f=os.path.join(sourcepath,f)
  if os.path.isdir(f):
    dirs_i.append(f)
  elif f.endswith('.dirs'):
    dirs_b+=dirlist_file(f)

if options.rebuild_everything:
  options.rebuild_all = True
  for pyver in py_supported:
    dir = os.path.join(basepath,pyver)
    if os.path.isdir(dir):
      os.spawnlp(os.P_WAIT, 'rm', 'rm', '-rf', dir)

# Check for changes in installed python versions
for pyver in py_oldversions+py_supported:
  dir = os.path.join(basepath,pyver)
  # Check for ".path" because sometimes the directory already exists 
  # while the python version isn't installed, because of some .so's.
  if pyver in py_installed and not os.path.isfile(os.path.join(dir,".path")):
    debug("Building all modules in %s..."%(dir))
    for basedir in dirs_i:
      process(basedir,install_modules([pyver]))
      process_extensions(basedir,install_modules,pyver)
    # Byte-compile after running install_modules
    bytecompile_all(pyver)
  if pyver not in py_installed and os.path.isdir(dir):
    debug("Removing obsolete directory %s..."%(dir))
    os.spawnlp(os.P_WAIT, 'rm', 'rm', '-rf', dir)

if options.rebuild_all:
  for basedir in dirs_b:
    process(basedir,clean_simple)
    bytecompile_privatedir(basedir)


# Now for the processing of what was handed on the command line
for basedir in do_dirs_b:
  if not options.clean_mode:
    bytecompile_privatedir(basedir)
  else:
    process(basedir,clean_simple)

to_bytecompile=to_clean=[]
for basedir in do_dirs_i:
  if not options.clean_mode:
    process(basedir,install_modules(py_installed))
    process_extensions(basedir,install_modules)
    to_bytecompile = concat(to_bytecompile,isect(dir_versions(basedir),py_installed))
  else:
    process(basedir,clean_modules)
    process_extensions(basedir,clean_modules_gen)
    to_clean = concat(to_clean,isect(dir_versions(basedir),py_installed))
# Byte-compile after running install_modules
for py in to_bytecompile:
  bytecompile_all(py)
# When removing a module, we have removed the .pyc but we still need
# to regenerate the .path file
for py in to_clean:
  generate_pathfile(py)
