#!/usr/bin/env python

# pufed.py -- use flags editor
# Copyright (C) 2004  Steven Hay -- hay.steve at gmail.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# The GNU Lesser General Public License can be obtained from:
# Free Software Foundation, Inc.
# 59 Temple Place, Suite 330
# Boston, MA  02111-1307  USA

try:
    import sets, portage, portage_util
except:
    print "You need to be running Gentoo to use this utility"
    
MAKECONF="/etc/make.conf"
DESCFILE=portage.settings["PORTDIR"] + "/profiles/use.desc"
LOCALDESCFILE=portage.settings["PORTDIR"] + "/profiles/use.local.desc"

def detectterm():
    """
    Semantics:  Detects the number of lines and columns in the terminal.
    Arguments:  None
    Returns:   ( lines, columns )   
    """
    import fcntl, termios, struct, sys
    return struct.unpack("HHHH",
			 fcntl.ioctl(sys.stdout.fileno(),
				     termios.TIOCGWINSZ,
				     struct.pack("HHHH", 0, 0, 0, 0)))[:2]

def demo(choices2):
    """
    Semantics:  User interface to get user-desired flags
    Arguments:  choices2 -- A list of menu item tuples.
                tuple -- (name, decription, selected)
    Returns:    List of selected options.
     """
    try:
	import dialog, sys
    except:
	print "Ensure you have pythondialog 2.7 installed."
	sys.exit(-1)

    try:
	d = dialog.Dialog(dialog="Xdialog")
    except:
	try:
	    d = dialog.Dialog(dialog="dialog")
	except:
	    print "You need to instaled dialog or Xdialog"
	    sys.exit(-1)

    d.add_persistent_args(["--backtitle", "pufed v0.1"])
    lines, cols = detectterm()
    try:
	while 1:
	    (code, tag) = d.checklist(text="", height=lines-6,
				      width=cols-4, list_height=lines-12,
				      choices=choices2,
				      title="Use flag selection",
				      backtitle="pufed v0.1")
	    if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
		sys.exit(0)
	    else:
		break
    except:
	print "Ensure you have pythondialog 2.7 isntalled."
	sys.exit(-1)
    return tag

def importUseLocalDesc(LOCALDESCFILE):
    """
    Semantics:  Imports the local use flag descriptions.
    Arguments:  LOCALDESCFILE -- The name of the file containing descriptions
    Returns:    ( useLocalDesc1, useLocalDesc2 ) where
                  useLocalDesc1 -- dict: descriptions keyed first on flag name
                  useLocalDesc2 -- dict: decsriptions keyed first on atom name
    """
    LOCALUSELISTRAW = portage_util.grabdict( LOCALDESCFILE )
    useLocalDesc1={}
    useLocalDesc2={}
    for uAtomFlag, uDescList in LOCALUSELISTRAW.items():
	uAtomFlagList = uAtomFlag.split(":")
	if len(uAtomFlagList) == 2:
	    uAtom=uAtomFlagList[0]
	    uFlag=uAtomFlagList[1]
	    uDesc=" ".join( uDescList[1:] )
	    useLocalDesc1.setdefault( uFlag, {} ).update( { uAtom : uDesc } )
	    useLocalDesc2.setdefault( uAtom, {} ).update( { uFlag : uDesc } )
    return (useLocalDesc1, useLocalDesc2)

def importUseDesc(DESCFILE):
    """
    Semantics:  Imports the global use flag descriptions
    Arguments:  DESCFILE -- The name of the file containing descriptions
    Returns:    dict: descriptions keyed on flag name
    """
    USELISTRAW = portage_util.grabdict( DESCFILE )
    useDesc={}
    for uFlag, uDescList in USELISTRAW.items():
	uDesc=" ".join( uDescList[1:] )
	useDesc[uFlag] = uDesc
	# print uFlag, ":\t", useDesc[uFlag]
    return useDesc

def importUseMask():
    """
    Semantics:  Imports the use mask from portage.
    Arguments:  None
    Returns:    Set: contains all masked flags
    """
    usemask_lists = portage_util.grab_multiple("use.mask",
					       portage.settings.profiles,
					       portage_util.grabfile)
    useMask = sets.Set( portage_util.stack_lists(usemask_lists,
						 incremental=True) )
    return useMask

def importArchList():
    """
    Semantics:  Get the arch flags.
    Arguments:  None
    Returns:    Set: Arch Flags
    """
    return sets.Set( portage.settings["PORTAGE_ARCHLIST"].split() )

def importUseFlags( settings ):
    """
    Semantics:  Get portage USE flags.
    Arguments:  settings -- portatge settings variable
    Returns:    Set: USE flags
    """
    return sets.Set( settings["USE"].split() )

def importMakeConf():
    """
    Semantics:  Imports the make.conf use flags from portage.
    Arguments:  None
    Returns:    Set: all make.conf use flags.
    """
    return importUseFlags( portage.settings )

def importNoMakeConf( exc="" ):
    """
    Semantics:  Imports the use flags as if make.conf had no use flags specified from portage
    Arguments:  except -- (optional) flags to include as an exception to the rule.
    Returns:    Set:  all use flags without affect of make.conf
    """
    settings = portage.config(clone=portage.settings);
    settings.configdict["conf"]["USE"] = exc;
    settings.reset();
    return importUseFlags( settings )

def makeChanges(useDesc, useMask, archList, useFlags):
    """
    Semantics:  Gets the USE flags selected by the user
    Arguments:  useDesc -- A dict of USE flag descriptions, keyed by flag name
                useMask -- A set of USE flags which cannot be changed
                useFlags -- A set of USE flags currently selected
    Returns:    set: Selected USE flags
    """
    useFlagsSet = sets.Set(useFlags)
    choices=[]
    for x,y in useDesc.items():
	if x not in useMask|archList:
	    if x in useFlags:
		choiceTuple=x,y,1
	    else:
		choiceTuple=x,y,0
	    choices.append( choiceTuple )
    choices.sort(lambda i,j: cmp(i[0].lower(), j[0].lower() ) )
    return sets.Set( demo(choices)) | (useFlags & archList)

def makeUseString( changes ):
    """
    Semantics:  creates the string to be inserted into the make.conf file
    Arguments:  changes -- set: The user-selected USE flags
                noMakeConf -- set: The USE flags if make.conf was empty
    Returns:    string: A valid USE=" ... " string for make.conf
    """
    useList=[]
    if "-*" in portage.settings.configdict['conf']['USE']:
	exc="-* "
    else:
	exc=""

    noMakeConf=importNoMakeConf( exc )

    # for x in changes-noMakeConf:
    #    useList.append(x)

    useList.extend(changes-noMakeConf)
    for x in noMakeConf-changes:
	useList.append("-" + x)
    return "USE=\"" + exc + " ".join(useList) + "\""

def loadMakeConf(MAKECONF):
    """
    Semantics:  Loads make.conf file
    Arguments:  The location of the system make.conf file
    Returns:    A list of lines in the make.conf file
    """
    makeConfFile=open(MAKECONF)
    mConfData=makeConfFile.readlines()
    makeConfFile.close()
    return mConfData

def backupMakeConf(MAKECONF):
    """
    Semantics:  Make a backup of the system make.conf file.
    Arguments:  The location of the system make.conf file
    Returns:    bool: Successful
    """
    import shutil
    try:
	shutil.copyfile(MAKECONF, MAKECONF+"~")
    except EnvironmentError:
	print "Backup " + MAKECONF + " not created."
	return False
    return True

def parseAndChange( fd, mConfData, useString):
    """
    Semantics:  Parses and saves the new make.conf file.
    Arguments:  fd -- file descriptor: the new make.conf
                mConfData -- list: lines in the old make.conf
		useString -- desired USE flags command
    Returns:    Nothing
    """
    useSet=False
    index=0
    while index<len(mConfData):
	line=mConfData[index]
	index+=1
	if False==line.lstrip().startswith("#"):
	    if line.lstrip().startswith("USE"):
		# handle the use flag changing
		while line.strip().endswith('\\'):
		    line=mConfData[index]
		    index+=1
                    if False==useSet:
			fd.write( useString + "\n" )
			useSet=True
	    else:
		fd.write( line )
	else:
	    fd.write(line )

def writeChanges( MAKECONF, useString ):
    """
    Semantics:  Saves the new make.conf file
    Arguments:  MAKECONF -- the location of the system make.conf file
                useString -- desired USE flags command
    Returns:    Nothing
    """
    # if the user has access to make.conf, back it up
    backupMakeConf(MAKECONF)

    ## load make.conf
    mConfData=loadMakeConf(MAKECONF)
    
    ## set stdout to make.conf
    try:
	makeConfFile=open(MAKECONF,"w")
    except EnvironmentError:
	makeConfFile=open("./make.conf", "w")
	print "Cannot write to " + MAKECONF + ".  Will use ./make.conf."

    parseAndChange( makeConfFile, mConfData, useString )
    print "The use flags defined in make.conf follow:"
    print useString

def main():
    # Grabs use.mask
    useMask=importUseMask()
    archList=importArchList()

    # grabs use flags with and without the effect of make.conf
    useFlags=importMakeConf()
    
    # imports use.local.desc and use.desc
    useLocalDescByFlag, useLocalDescByAtom = importUseLocalDesc(LOCALDESCFILE)
    useDesc=importUseDesc(DESCFILE)
    
    # creates complete list of use flags
    allUseDesc=useDesc.copy()
    for uFlag, uAtomDesc in useLocalDescByFlag.items():
	uDescTotal=""
	for uAtom, uDesc in uAtomDesc.items():
	    uDescTotal= uDescTotal + uDesc + " "
	allUseDesc[uFlag] = uDescTotal

    newUseFlags=makeChanges( allUseDesc, useMask, archList, useFlags )

    # create the USE definition string to replace the old one
    useString=makeUseString(newUseFlags)
    
    writeChanges(MAKECONF, useString)


if __name__ == '__main__': main()

     
