import os
import re
import shutil


def findFramework(path, name = None):
	"find the framework folder for FW name"
	if path == "" or path == "@executable_path" or path == "/":
		return None
	elif name == None:
		return findFramework(os.path.dirname(path), 
							 os.path.basename(path))
	elif os.path.basename(path) == name + ".framework":
		return path
	elif len(os.path.dirname(path)) >= len(path):
		print "MachO.findFramework: Oops '" + path + "', '" + name + "'"
		return None
	else:
		return findFramework(os.path.dirname(path), name)
	


def stripPrefix(prefix, path):
	"Returns the relative path r such that os.path.join(prefix, r) == path"
	prefix = os.path.normpath(prefix)
	prefixLen = len(prefix)
	path = os.path.normpath(path)
	if path[0 : prefixLen] == prefix:
		if path[prefixLen] == os.sep:
			return path[prefixLen+1 : ]
		else:
			return path[prefixLen : ]
	else:
		return path
		
		
class Executable:
	"Represents an Mach-O executable."	
	
	def __init__(self, path, kind):
		self.Location = path
		self.Kind = kind
		
		
	def __repr__(self):
		return self.Location + " (" + self.Kind + ")"
		
		
	def getDependencies(self):
		"Return a list of MachO.Fixes describing the dependencies."
		"Uses otool -L"
		f = os.popen("otool -L " + self.Location, "r")
		result = []
		pat = re.compile("\s*([^(]*)\s\((.+)\)")
		for line in f:
			m = pat.match(line)
			if m != None:
				result.append(Fix(m.group(1), m.group(2)))
		status = f.close()
		return result
		
	def applyFixes(self, changes, log):
		"Uses install_name_tool to change the links to dependencies."
		"changes is a dictionary mapping links (as strings) to Fixes."
		args = ""
		for dep in self.getDependencies():
			if dep.Link in changes:
				args = args + changes[dep.Link].getChange()
		log.append(">> " + "install_name_tool " + args + self.Location)
		if len(args) > 0:
			os.system("install_name_tool " + args + self.Location)
		pat = re.compile("(library)|(universal binary)")
		if pat.search(self.Kind):
			relName = os.path.basename(self.Location) # FIXME: rel to fw
			log.append(">> " + "install_name_tool -id " + relName + 
						" " + self.Location)
			os.system("install_name_tool -id " + relName + 
						" " + self.Location)


def findExecutables(bundleDir):
	"Return a list of MachO.Executables found in bundleDir"
	result = []
	pat = re.compile("Mach-O (.+)")
	for root, dirs, files in os.walk(bundleDir):
		for n in files:
			p = os.path.join(root, n)
			f = os.popen("file -b " + p, "r")
			m = pat.match(f.readline())
			if m != None:
				result.append(Executable(p, m.group(1)))
				print "found " + m.group(1) + ": " + n
			f.close()
	return result



class Fix:
	"Represents a fix for a library link."
	
	def __init__(self, dependency, versionString="?"):
		self.Link = dependency
		self.Location = dependency
		self.NewLink = dependency
		self.NewLocation = dependency
		self.versionString = versionString # not used yet
		self.fwPath = None
		self.relPath = None
		
	def __repr__(self):
		return (self.Link + " (" + self.versionString + ")")
		
	def isAbsolute(self):
		return os.path.isabs(self.Link)
		
	def isBundleRelative(self):
		return self.Link[0:17] == "@executable_path/"
		
	def isSystem(self):
		return (self.Location[0:8] == "/usr/lib"     # also matches libexec
				or self.Location[0:8] == "/usr/X11"  # also matches X11R6
				or self.Location[0:8] == "/System/")
				
	def getChange(self):
		"Returns argument for install_name_tool."
		if self.Link == self.NewLink:
			return ""
		else:
			return "-change " + self.Link + " " + self.NewLink + " "
		
	def findLocation(self, exePath=None):
		if self.isBundleRelative():
			if exePath != None:
				self.Location = os.path.normpath(
								os.path.join(exePath, self.Link[17:]))
			else:
				self.Location = self.Link[17:]
		else:
			self.Location = self.Link

		# check if done
		if (os.path.isabs(self.Location) and
			os.path.isfile(self.Location)):
			self.NewLocation = self.Location
			return True
		
		# search for frameworks in /System/Library and /Library
		fwPath = findFramework(self.Location)
		if fwPath:
			fwdir = os.path.dirname(fwPath)
			self.relPath = stripPrefix(fwdir, self.Location)
			for d in	["/Library/Frameworks", 
						"/System/Library/Frameworks"]:
				if os.path.isfile(os.path.join(d, self.relPath)):
#					self.Location = os.path.join(d, self.relPath)
					self.Location = os.path.join(d, self.relPath)
					self.NewLocation = self.Location
					self.fwPath = os.path.join(d, os.path.basename(fwPath))
					self.relPath = stripPrefix(self.fwPath, self.Location)
					return True
		
		# ok, try libs
		lib = os.path.basename(self.Location)
		self.relPath = None
		for d in	["/usr/local/lib", "/opt/local/lib", 
					"/usr/lib", "/opt/lib"]:
			if os.path.isfile(os.path.join(d, lib)):
				self.Location = os.path.join(d, lib)
				self.NewLocation = self.Location
				return True

		# not found
		return False
		
		
	def moveLibrary(self, destBundlePath, stripFW, log):
		"Copies the library or fw to destBundlePath."
		"Also sets NewLink and NewLocation properties"
		"Returns a list of copied executables"
		
		# dont do this if we are already inside the bundle:
		if stripPrefix(destBundlePath, self.Location) != self.Location:
			log.append("-- ignoring " + self.Location)
			return []
		
		if self.relPath != None and not stripFW:
			# copy framework
			newFwPath = os.path.join(destBundlePath, 
										"Contents/Frameworks", 
										os.path.basename(self.fwPath))
			log.append(">> " + self.fwPath + " ===> " + newFwPath)
			if (os.path.exists(destBundlePath) and
				not os.path.exists(newFwPath)):
				shutil.copytree(self.fwPath, newFwPath, True)
			self.NewLocation = os.path.join(newFwPath, self.relPath)
			self.NewLink = ("@executable_path/" 
							+ os.path.join("../Frameworks", 
											os.path.basename(self.fwPath),
											self.relPath))
			return findExecutables(newFwPath)
		else:
			# copy lib to bundle.app/Contents/Frameworks/
			self.NewLocation = os.path.join(destBundlePath, 
											"Contents/Frameworks",
											os.path.basename(self.Location))
			self.NewLink = ("@executable_path/" 
							+ os.path.join("../Frameworks", 
											os.path.basename(self.Location)))
			log.append(">> " + self.Location + " ---> " + self.NewLocation)
			if (os.path.exists(destBundlePath) and
				not os.path.exists(self.NewLocation)):
				shutil.copy(self.Location, self.NewLocation)
			return [Executable(self.NewLocation, "lib")]
			
	