Difference between revisions of "MakeStaticPackage.py Source Code"

From dftwiki3
Jump to: navigation, search
(Created page with "--~~~~ ---- <bluebox> This Python code is taken from a Google application, and is reproduced here as an example of the use of functions. In particular, note how some functio...")
 
Line 2: Line 2:
 
----
 
----
 
<bluebox>
 
<bluebox>
This Python code is taken from a Google application, and is reproduced here as an example of the use of functions.  In particular, note how some functions are fairly short, while others longer.  In general most are less than the equivalent of a screen height, so that you can easily read the whole function at without sliding the code up or down, making it easy to fully understand what the function does.  Functions are mini programs, in a way, and have their own logic, their own entry point, and their own exit point(s).
+
This Python code is a utility program I wrote to help me generate some particular type of applications, and is reproduced here as an example of the use of functions.  In particular, note how some functions are fairly short, while others longer.  In general most are less than the equivalent of a screen height, so that you can easily read the whole function at without sliding the code up or down, making it easy to fully understand what the function does.  Functions are mini programs, in a way, and have their own logic, their own entry point, and their own exit point(s).
 
</bluebox>
 
</bluebox>
  
 
<br />
 
<br />
 
<source lang="python">
 
<source lang="python">
# Copyright (C) 2012 Google, Inc.
+
#! /usr/bin/env python
# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+
# makeStaticPackage.py
 +
# (C) D. Thiebaut
 
#
 
#
# Redistribution and use in source and binary forms, with or without
+
# This program runs under Mac OS X.
# modification, are permitted provided that the following conditions
+
 
# are met:
+
# This program takes the executable generated by Qt3 (but would work
# 1. Redistributions of source code must retain the above copyright
+
# very likely with other compiled output) that relies on Qt3 specific
#    notice, this list of conditions and the following disclaimer.
+
# libraries that may not be available on the clients of an XGrid system.
# 2Redistributions in binary form must reproduce the above copyright
+
#
#    notice, this list of conditions and the following disclaimer in the
+
# The program should be run in an empty directory.  It is important as
#    documentation and/or other materials provided with the distribution.
+
# it will copy in this directory the executable and all the libraries it
 +
# depends on and will modify all of them.  So, make sure you start in
 +
# an empty directory.  All you need to know is the path to the executable
 +
# you want to deploy on the XGrid.
 +
#
 +
# This program is given the path of an executable that should be somewhere
 +
# but NOT IN THE CURRENT DIRECTORY:
 +
#
 +
#        makeStaticPackage.py  ~/bin/filterwiki9
 +
#
 +
# The algorithm is the following:
 +
#  1) copy the executable in the current directory
 +
# 2) extract the list of libraries used by the executable using otool
 +
3) keep the names of the libraries that reside in /opt/local/lib
 +
#    as they very likely won't be on the XGrid clients.
 +
#  4) modify the executable and replace the full path reference to these
 +
#     /opt/local/lib libraries and use @executable_path insteadThis
 +
#    way the library can be distributed with the executable and run from
 +
#    the working directory.
 +
#  5) copy the /opt/local/lib libraries to the current directory, and
 +
#    apply steps 1 to 4 above to each one.  Keep on going until the
 +
#    graph of all libraries relating to /opt/local/lib is complete.
 +
#
 +
# Here is an example of the executable filterwiki9 before it is processed
 +
#
 +
#  otool -L filterwiki9
 +
#  filterwiki9:
 +
# /opt/local/lib/libqt-mt.3.dylib (compatibility version 3.3.0, current version 3.3.8)
 +
# /opt/local/lib/libXext.6.dylib (compatibility version 11.0.0, current version 11.0.0)
 +
# /opt/local/lib/libX11.6.dylib (compatibility version 10.0.0, current version 10.0.0)
 +
# /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 111.1.5)
 +
# /usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.4.0)
 +
# /usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)
 
#
 
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
+
# and after processing:
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+
#
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+
# @executable_path/libqt-mt.3.dylib (compatibility version 3.3.0, current version 3.3.8)
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
+
# @executable_path/libXext.6.dylib (compatibility version 11.0.0, current version 11.0.0)
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+
# @executable_path/libX11.6.dylib (compatibility version 10.0.0, current version 10.0.0)
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+
# /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 111.1.5)
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+
# /usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.4.0)
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+
# /usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+
#
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
# Note that only the entries for /opt/local/lib libraries are changed.
 +
#
 +
# Versions:
 +
# Version 3: 6/10/2010 Can now process several executables entered on the command line
 +
#
 +
# Version 2: 6/9/2010  Simplified it and remove the processing of data files.
 +
#                      Outputs a list of the files that were brought in.
 +
#
 +
VERSION = "V3 (6/10/2010)"
 +
 
 +
import sys
 +
import os
 +
import subprocess
  
"""this module is responsible for finding python tests."""
+
copiedLibs = {}
  
import logging
 
import re
 
  
 +
copiedDico = {}    # the dico of all copied files
 +
processedDico = {} # the dico of all processed libraries
 +
                  # a processed library is one that has been
 +
                  # copied and whose library entries have ahd
 +
                  # their path changed to local
  
_log = logging.getLogger(__name__)
+
def baseNameOf( fullPath ):
 +
    return fullPath.split( '/' )[-1]
  
 +
def safeCopyHere( fullPath, Debug=False ):
 +
    """copies file from remote library dir to current dir.
 +
    and updates dictionary of copied files"""
  
class _DirectoryTree(object):
+
    baseName = baseNameOf( fullPath )
     def __init__(self, filesystem, top_directory, starting_subdirectory):
+
     #--- do not copy if we already have it (as it might have been processed ---
        self.filesystem = filesystem
+
    #--- already!)                                                         ---
        self.top_directory = filesystem.realpath(top_directory)
+
    if baseName in copiedDico:
        self.search_directory = self.top_directory
+
         if Debug: print "safeCopyHere: file %s already copied. Skipping..." % baseName
        self.top_package = ''
+
        return
         if starting_subdirectory:
 
            self.top_package = starting_subdirectory.replace(filesystem.sep, '.') + '.'
 
            self.search_directory = filesystem.join(self.top_directory, starting_subdirectory)
 
  
     def find_modules(self, suffixes, sub_directory=None):
+
     #--- bring into current directory ---
         if sub_directory:
+
    try:
             search_directory = self.filesystem.join(self.top_directory, sub_directory)
+
        subprocess.check_call( ["cp", fullPath, "." ] )
 +
    except:
 +
        print "could not copy %s to current directory" % fullPath
 +
        sys.exit( 1 )
 +
 
 +
    #--- make it writable (some are not by default) ---
 +
    try:
 +
        subprocess.check_call( ["chmod", "+w", baseName ] )
 +
    except:
 +
        print "could not chmod +x %s" % baseName
 +
        sys.exit( 1 )
 +
 
 +
    #--- record it as copied! ---
 +
    copiedDico[ fullPath ] = True
 +
    if Debug: print "safeCopyHere: copied file %s from %s" % ( baseName, fullPath )
 +
 
 +
 
 +
def changePathsToLocal( fullPath, Debug=False ):
 +
    """takes the library or executable specified by fullPath and marks them
 +
    as local in the code of fullPath.  Returns the list of libraries found
 +
    as a list of full paths"""
 +
 
 +
    if Debug:
 +
        print "\n--------------------------------------------------------"
 +
        print "changePathsToLocal( %s )" % fullPath
 +
        print "--------------------------------------------------------"
 +
       
 +
    baseName = baseNameOf( fullPath )
 +
 
 +
    if not fullPath in copiedDico:
 +
        print "*** Error: changePathsToLocal cannot process %s: hasn't been copied yet ***" % baseName
 +
        sys.exit( 1 )
 +
 
 +
    if fullPath in processedDico:
 +
        if Debug: print "changePathsToLocal: %s already processed.  Skipping" % baseName
 +
        return []
 +
   
 +
    #--- get libraries ---
 +
    output = subprocess.Popen(["otool", "-L",  baseName ],
 +
                              stdout=subprocess.PIPE).communicate()[0]
 +
 
 +
    if Debug:
 +
        print "changePathsToLocal: otool -L", baseName
 +
        print "changePathsToLocal:", output
 +
       
 +
    #--- extract library names ---
 +
    firstLevelLibs  = []
 +
    for i, line in enumerate( output.split( '\n' ) ):
 +
        line = line.strip()
 +
         if line.find( "/opt/local" ) != -1:
 +
             fullPathLib = line.split( ' ' )[0]
 +
            firstLevelLibs.append( fullPathLib )
 +
 
 +
    #--- change library path in fullPath to local ---
 +
    for fullPathLib in firstLevelLibs:
 +
        baseNameLib = baseNameOf( fullPathLib )
 +
        if Debug: print "changePathsToLocal: changing path of %s in %s," % (baseNameLib, baseName)
 +
 
 +
        if baseNameLib == baseName :
 +
            output = subprocess.Popen( [ "install_name_tool",  
 +
                                        "-id",
 +
                                        "@executable_path/" + baseNameLib,
 +
                                        baseName ],
 +
                                      stdout=subprocess.PIPE).communicate()[0]
 +
           
 
         else:
 
         else:
             search_directory = self.search_directory
+
             output = subprocess.Popen( [ "install_name_tool",
 +
                                        "-change",
 +
                                        fullPathLib,
 +
                                        "@executable_path/" + baseNameLib,
 +
                                        baseName ],
 +
                                      stdout=subprocess.PIPE).communicate()[0]
 +
           
 +
    #--- record this entry as processed! --
 +
    processedDico[ fullPath ] = True       
 +
    if Debug:
 +
        output = subprocess.Popen(["otool", "-L",  baseName ],
 +
                                  stdout=subprocess.PIPE).communicate()[0]
 +
        print "changePathsToLocal: otool -L", baseName
 +
        print "changePathsToLocal:", output
 +
        print "changePathsToLocal finished processing %s" % baseName
 +
        print "--------------------------------------------------------\n"
 +
       
 +
    return firstLevelLibs
 +
 
 +
def process( executable, Debug=False ):
 +
    """figures out what libraries executable relies on that are not system libraries,
 +
    and are in /opt/local, copies them to the local directory and marks them as
 +
    local in the code of executable."""
  
        def file_filter(filesystem, dirname, basename):
+
    #--- in case the executable comes from far away ---
            return any(basename.endswith(suffix) for suffix in suffixes)
+
    safeCopyHere( executable, Debug )
 +
    baseExecutable = baseNameOf( executable )
 +
   
 +
    #--- we assume executable is the code file compiled by Qt and the starting point  ---
 +
    #--- of the process. It does not need to be copied.                              ---
 +
    fullPathLibs = changePathsToLocal( executable, Debug )
  
         filenames = self.filesystem.files_under(search_directory, file_filter=file_filter)
+
    #--- assumption: fullPathLibs is a list containing libraries that must be brought in ---
         return [self.to_module(filename) for filename in filenames]
+
    #--- and processed.  If a new library is found and
 +
    while len( fullPathLibs )>0:
 +
         fullPath = fullPathLibs.pop()
 +
        if Debug: print "<--- Dequeueing ", fullPath
 +
        baseName = baseNameOf( fullPath )
 +
        safeCopyHere( fullPath, Debug )
 +
         newLibsFound = changePathsToLocal( fullPath, Debug )
 +
        for lib in newLibsFound:
 +
            if not lib in processedDico and not lib in fullPathLibs:
 +
                fullPathLibs.insert( 0, lib )
 +
                if Debug: print "---> Enqueueing ", lib
  
    def to_module(self, path):
 
        return path.replace(self.top_directory + self.filesystem.sep, '').replace(self.filesystem.sep, '.')[:-3]
 
  
    def subpath(self, path):
+
# ---------------------------------------------------------------------------------------------
        """Returns the relative path from the top of the tree to the path, or None if the path is not under the top of the tree."""
+
#                   
        realpath = self.filesystem.realpath(self.filesystem.join(self.top_directory, path))
+
#                                __  __      _      ___  _  _
        if realpath.startswith(self.top_directory + self.filesystem.sep):
+
#                              |  \/  |    / \    |_ _| | \ | |
            return realpath.replace(self.top_directory + self.filesystem.sep, '')
+
#                              | |\/| |  / _ \    | |  |  \| |
        return None
+
#                              | |  | |  / ___ \  | |  | |\  |
 +
#                              |_|  |_| /_/  \_\ |___| |_| \_|
 +
#
 +
# ---------------------------------------------------------------------------------------------
 +
#
 +
def main( Debug=False ):
 +
    global VERSION
  
     def clean(self):
+
     #--- Syntax ---
         """Delete all .pyc files in the tree that have no matching .py file."""
+
    args = sys.argv
        _log.debug("Cleaning orphaned *.pyc files from: %s" % self.search_directory)
+
    if len( args ) < 2:
         filenames = self.filesystem.files_under(self.search_directory)
+
         print "syntax: %s MacExecutableFile [[MacExecutableFile]...] [-clear]" % args[0]
         for filename in filenames:
+
         print
            if filename.endswith(".pyc") and filename[:-1] not in filenames:
+
         print  "        -clear: if appears in args, then all dylib files are erased first.\n\n"
                _log.info("Deleting orphan *.pyc file: %s" % filename)
+
        print  "       Version: ", VERSION
                self.filesystem.remove(filename)
+
        return
  
 +
    #--- get executable ---
 +
    executables = []
  
class Finder(object):
+
    clearDyLib = False
     def __init__(self, filesystem):
+
     for i in range( 1, len( args ) ):
         self.filesystem = filesystem
+
         if args[i].lower()=="-clear":
        self.trees = []
+
            clearDyLib = True
         self._names_to_skip = []
+
         else:
 +
            executables.append( args[i] )
  
     def add_tree(self, top_directory, starting_subdirectory=None):
+
     #--- remove all the current *.dylib in directory ---
        self.trees.append(_DirectoryTree(self.filesystem, top_directory, starting_subdirectory))
+
    if clearDyLib:
 +
        try:
 +
            subprocess.check_call( ["rm", "-f", "*.dylib" ] )
 +
        except:
 +
            print "could not erase all dylib files"
 +
            sys.exit( 1 )
  
     def skip(self, names, reason, bugid):
+
     #--- create a network of libs in /opt/local/libs, bring them to        ---
         self._names_to_skip.append(tuple([names, reason, bugid]))
+
    #--- the local directory, and mark all their internal paths to other  ---
 +
    #--- /opt/local/lib libraries as local.  Bring all the ones listed and ---
 +
    #--- treat them as well.                                              ---
 +
    for executable in executables:
 +
         if Debug: print "calling process( %s )" % executable
 +
        process( executable, Debug )
  
     def additional_paths(self, paths):
+
     #--- summary info ---
        return [tree.top_directory for tree in self.trees if tree.top_directory not in paths]
+
    for k in processedDico.keys():
 +
            print "-->", k.split( '/' )[-1]
  
    def clean_trees(self):
 
        for tree in self.trees:
 
            tree.clean()
 
  
    def is_module(self, name):
+
main( False ) # True )
        relpath = name.replace('.', self.filesystem.sep) + '.py'
+
 
        return any(self.filesystem.exists(self.filesystem.join(tree.top_directory, relpath)) for tree in self.trees)
 
  
    def is_dotted_name(self, name):
 
        return re.match(r'[a-zA-Z.][a-zA-Z0-9_.]*', name)
 
  
    def to_module(self, path):
 
        for tree in self.trees:
 
            if path.startswith(tree.top_directory):
 
                return tree.to_module(path)
 
        return None
 
  
    def find_names(self, args, find_all):
 
        suffixes = ['_unittest.py', '_integrationtest.py']
 
        if args:
 
            names = []
 
            for arg in args:
 
                names.extend(self._find_names_for_arg(arg, suffixes))
 
            return names
 
  
        return self._default_names(suffixes, find_all)
 
  
    def _find_names_for_arg(self, arg, suffixes):
 
        realpath = self.filesystem.realpath(arg)
 
        if self.filesystem.exists(realpath):
 
            names = self._find_in_trees(realpath, suffixes)
 
            if not names:
 
                _log.error("%s is not in one of the test trees." % arg)
 
            return names
 
  
        # See if it's a python package in a tree (or a relative path from the top of a tree).
 
        names = self._find_in_trees(arg.replace('.', self.filesystem.sep), suffixes)
 
        if names:
 
            return names
 
  
        if self.is_dotted_name(arg):
 
            # The name may not exist, but that's okay; we'll find out later.
 
            return [arg]
 
  
        _log.error("%s is not a python name or an existing file or directory." % arg)
 
        return []
 
  
    def _find_in_trees(self, path, suffixes):
 
        for tree in self.trees:
 
            relpath = tree.subpath(path)
 
            if not relpath:
 
                continue
 
            if self.filesystem.isfile(path):
 
                return [tree.to_module(path)]
 
            else:
 
                return tree.find_modules(suffixes, path)
 
        return []
 
  
    def _default_names(self, suffixes, find_all):
 
        modules = []
 
        for tree in self.trees:
 
            modules.extend(tree.find_modules(suffixes))
 
        modules.sort()
 
  
        for module in modules:
 
            _log.debug("Found: %s" % module)
 
  
        if not find_all:
 
            for (names, reason, bugid) in self._names_to_skip:
 
                self._exclude(modules, names, reason, bugid)
 
  
        return modules
 
  
    def _exclude(self, modules, module_prefixes, reason, bugid):
 
        _log.info('Skipping tests in the following modules or packages because they %s:' % reason)
 
        for prefix in module_prefixes:
 
            _log.info('    %s' % prefix)
 
            modules_to_exclude = filter(lambda m: m.startswith(prefix), modules)
 
            for m in modules_to_exclude:
 
                if len(modules_to_exclude) > 1:
 
                    _log.debug('        %s' % m)
 
                modules.remove(m)
 
        _log.info('    (https://bugs.webkit.org/show_bug.cgi?id=%d; use --all to include)' % bugid)
 
        _log.info('')
 
  
  

Revision as of 09:33, 23 February 2015

--D. Thiebaut (talk) 09:26, 23 February 2015 (EST)


This Python code is a utility program I wrote to help me generate some particular type of applications, and is reproduced here as an example of the use of functions. In particular, note how some functions are fairly short, while others longer. In general most are less than the equivalent of a screen height, so that you can easily read the whole function at without sliding the code up or down, making it easy to fully understand what the function does. Functions are mini programs, in a way, and have their own logic, their own entry point, and their own exit point(s).


#! /usr/bin/env python
# makeStaticPackage.py
# (C) D. Thiebaut
#
# This program runs under Mac OS X.

# This program takes the executable generated by Qt3 (but would work
# very likely with other compiled output) that relies on Qt3 specific
# libraries that may not be available on the clients of an XGrid system.
#
# The program should be run in an empty directory.  It is important as
# it will copy in this directory the executable and all the libraries it
# depends on and will modify all of them.  So, make sure you start in
# an empty directory.  All you need to know is the path to the executable
# you want to deploy on the XGrid.
#
# This program is given the path of an executable that should be somewhere
# but NOT IN THE CURRENT DIRECTORY:
#
#         makeStaticPackage.py  ~/bin/filterwiki9 
#
# The algorithm is the following:
#  1) copy the executable in the current directory
#  2) extract the list of libraries used by the executable using otool
#  3) keep the names of the libraries that reside in /opt/local/lib
#     as they very likely won't be on the XGrid clients.
#  4) modify the executable and replace the full path reference to these
#     /opt/local/lib libraries and use @executable_path instead.  This
#     way the library can be distributed with the executable and run from
#     the working directory.
#  5) copy the /opt/local/lib libraries to the current directory, and
#     apply steps 1 to 4 above to each one.  Keep on going until the
#     graph of all libraries relating to /opt/local/lib is complete.
#
# Here is an example of the executable filterwiki9 before it is processed
#
#   otool -L filterwiki9
#   filterwiki9:
#	/opt/local/lib/libqt-mt.3.dylib (compatibility version 3.3.0, current version 3.3.8)
#	/opt/local/lib/libXext.6.dylib (compatibility version 11.0.0, current version 11.0.0)
#	/opt/local/lib/libX11.6.dylib (compatibility version 10.0.0, current version 10.0.0)
#	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 111.1.5)
#	/usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.4.0)
#	/usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)
#
# and after processing:
#
#	@executable_path/libqt-mt.3.dylib (compatibility version 3.3.0, current version 3.3.8)
#	@executable_path/libXext.6.dylib (compatibility version 11.0.0, current version 11.0.0)
#	@executable_path/libX11.6.dylib (compatibility version 10.0.0, current version 10.0.0)
#	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 111.1.5)
#	/usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.4.0)
#	/usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)
#
# Note that only the entries for /opt/local/lib libraries are changed.
# 
# Versions:
# Version 3: 6/10/2010 Can now process several executables entered on the command line
# 
# Version 2: 6/9/2010  Simplified it and remove the processing of data files.
#                      Outputs a list of the files that were brought in.
#
VERSION = "V3 (6/10/2010)"

import sys
import os
import subprocess

copiedLibs = {}


copiedDico = {}    # the dico of all copied files
processedDico = {} # the dico of all processed libraries
                   # a processed library is one that has been
                   # copied and whose library entries have ahd
                   # their path changed to local

def baseNameOf( fullPath ):
    return fullPath.split( '/' )[-1]

def safeCopyHere( fullPath, Debug=False ):
    """copies file from remote library dir to current dir.
    and updates dictionary of copied files"""

    baseName = baseNameOf( fullPath )
    #--- do not copy if we already have it (as it might have been processed ---
    #--- already!)                                                          ---
    if baseName in copiedDico:
        if Debug: print "safeCopyHere: file %s already copied. Skipping..." % baseName
        return

    #--- bring into current directory ---
    try:
        subprocess.check_call( ["cp", fullPath, "." ] )
    except:
        print "could not copy %s to current directory" % fullPath
        sys.exit( 1 )

    #--- make it writable (some are not by default) ---
    try:
        subprocess.check_call( ["chmod", "+w", baseName ] )
    except:
        print "could not chmod +x %s" % baseName
        sys.exit( 1 )

    #--- record it as copied! ---
    copiedDico[ fullPath ] = True
    if Debug: print "safeCopyHere: copied file %s from %s" % ( baseName, fullPath )


def changePathsToLocal( fullPath, Debug=False ):
    """takes the library or executable specified by fullPath and marks them
    as local in the code of fullPath.  Returns the list of libraries found
    as a list of full paths"""

    if Debug:
        print "\n--------------------------------------------------------"
        print "changePathsToLocal( %s )" % fullPath
        print "--------------------------------------------------------"
        
    baseName = baseNameOf( fullPath )

    if not fullPath in copiedDico:
        print "*** Error: changePathsToLocal cannot process %s: hasn't been copied yet ***" % baseName
        sys.exit( 1 )

    if fullPath in processedDico:
        if Debug: print "changePathsToLocal: %s already processed.  Skipping" % baseName
        return []
    
    #--- get libraries ---
    output = subprocess.Popen(["otool", "-L",  baseName ], 
                              stdout=subprocess.PIPE).communicate()[0]

    if Debug:
        print "changePathsToLocal: otool -L", baseName
        print "changePathsToLocal:", output
        
    #--- extract library names ---
    firstLevelLibs  = []
    for i, line in enumerate( output.split( '\n' ) ):
        line = line.strip()
        if line.find( "/opt/local" ) != -1:
            fullPathLib = line.split( ' ' )[0] 
            firstLevelLibs.append( fullPathLib )

    #--- change library path in fullPath to local ---
    for fullPathLib in firstLevelLibs:
        baseNameLib = baseNameOf( fullPathLib )
        if Debug: print "changePathsToLocal: changing path of %s in %s," % (baseNameLib, baseName)

        if baseNameLib == baseName :
            output = subprocess.Popen( [ "install_name_tool", 
                                         "-id",
                                         "@executable_path/" + baseNameLib,
                                         baseName ],
                                       stdout=subprocess.PIPE).communicate()[0] 
            
        else:
            output = subprocess.Popen( [ "install_name_tool", 
                                         "-change",
                                         fullPathLib,
                                         "@executable_path/" + baseNameLib,
                                         baseName ],
                                       stdout=subprocess.PIPE).communicate()[0]
            
    #--- record this entry as processed! --
    processedDico[ fullPath ] = True        
    if Debug:
        output = subprocess.Popen(["otool", "-L",  baseName ], 
                                  stdout=subprocess.PIPE).communicate()[0]
        print "changePathsToLocal: otool -L", baseName
        print "changePathsToLocal:", output
        print "changePathsToLocal finished processing %s" % baseName
        print "--------------------------------------------------------\n"
        
    return firstLevelLibs

def process( executable, Debug=False ):
    """figures out what libraries executable relies on that are not system libraries,
    and are in /opt/local, copies them to the local directory and marks them as
    local in the code of executable."""

    #--- in case the executable comes from far away ---
    safeCopyHere( executable, Debug )
    baseExecutable = baseNameOf( executable )
    
    #--- we assume executable is the code file compiled by Qt and the starting point  ---
    #--- of the process.  It does not need to be copied.                              ---
    fullPathLibs = changePathsToLocal( executable, Debug )

    #--- assumption: fullPathLibs is a list containing libraries that must be brought in ---
    #--- and processed.  If a new library is found and 
    while len( fullPathLibs )>0:
        fullPath = fullPathLibs.pop()
        if Debug: print "<--- Dequeueing ", fullPath
        baseName = baseNameOf( fullPath )
        safeCopyHere( fullPath, Debug )
        newLibsFound = changePathsToLocal( fullPath, Debug )
        for lib in newLibsFound:
            if not lib in processedDico and not lib in fullPathLibs:
                fullPathLibs.insert( 0, lib )
                if Debug: print "---> Enqueueing ", lib


# ---------------------------------------------------------------------------------------------
#                     
#                                __  __      _      ___   _   _ 
#                               |  \/  |    / \    |_ _| | \ | |
#                               | |\/| |   / _ \    | |  |  \| |
#                               | |  | |  / ___ \   | |  | |\  |
#                               |_|  |_| /_/   \_\ |___| |_| \_|
#
# ---------------------------------------------------------------------------------------------
#
def main( Debug=False ):
    global VERSION

    #--- Syntax ---
    args = sys.argv
    if len( args ) < 2:
        print "syntax: %s MacExecutableFile [[MacExecutableFile]...] [-clear]" % args[0]
        print 
        print  "        -clear: if appears in args, then all dylib files are erased first.\n\n"
        print  "        Version: ", VERSION
        return

    #--- get executable ---
    executables = []

    clearDyLib = False
    for i in range( 1, len( args ) ):
        if args[i].lower()=="-clear":
            clearDyLib = True
        else:
            executables.append( args[i] )

    #--- remove all the current *.dylib in directory ---
    if clearDyLib:
        try:
            subprocess.check_call( ["rm", "-f", "*.dylib" ] )
        except:
            print "could not erase all dylib files"
            sys.exit( 1 )

    #--- create a network of libs in /opt/local/libs, bring them to        ---
    #--- the local directory, and mark all their internal paths to other   ---
    #--- /opt/local/lib libraries as local.  Bring all the ones listed and ---
    #--- treat them as well.                                               ---
    for executable in executables:
        if Debug: print "calling process( %s )" % executable
        process( executable, Debug )

    #--- summary info ---
    for k in processedDico.keys():
            print "-->", k.split( '/' )[-1]


main( False ) # True )