#Copyright ReportLab Europe Ltd. 2000-2004
#see license.txt for license details
__version__=''' $Id: zipapp.py 15198 2004-11-02 14:12:37Z rgbecker $ '''
__all__=('ZipAppDef','ArgProcessor','ZipAppArchive','StandardDistros')
import sys, os, time, shutil, fnmatch
from buildutils import  kill, chdir, path_join,\
                        path_dirname, isFile,\
                        isDir, fetchRL, svn_info, listAllFiles, listAllDirs,\
                        errorMsg, path_basename, findSVNURL,\
                        svn_update, stagingArea, rename
from glob import glob
import rlextra, reportlab
from rlextra.utils.superglob import FileSet, SuperFileSet
from reportlab.lib.utils import get_rl_tempdir

class StandardDistros:
    pfb = 0
    def reportlabDistro(self,srcDir,full=1,buildDir=None):
        fs = FileSet(srcDir)
        fs.include('reportlab/**/*')
        fs.exclude('reportlab/**/*.pyc')
        fs.exclude('reportlab/**/*.pyo')
        fs.exclude('reportlab/**/CVS/*')
        fs.exclude('reportlab/**/.svn/**/*')
        fs.exclude('reportlab/**/PROJECT_CVS_TAG')
        fs.exclude('reportlab/**/filelist.txt')     #if we need these wel'll make our own I suppose
        sfs = SuperFileSet(fs)
        if not full:
            sfs.exclude('reportlab/graphics/samples/**/*')
            sfs.exclude('reportlab/demos/**/*')
            sfs.exclude('reportlab/docs/**/*')
            sfs.exclude('reportlab/test/**/*')
            sfs.exclude('reportlab/tools/**/*')
            sfs.exclude('reportlab/**/README.*')
            sfs.exclude('reportlab/**/README')
            sfs.exclude('reportlab/**/VERSION')
            sfs.exclude('reportlab/**/TODO')
            sfs.exclude('reportlab/**/changes')
            if not self.pfb:
                sfs.exclude('reportlab/fonts/**/*')
            sfs.exclude('reportlab/**/*.c')
            sfs.exclude('reportlab/**/*.h')
            sfs.exclude('reportlab/**/*.dsw')
            sfs.exclude('reportlab/**/*.dsp')
            sfs.exclude('reportlab/**/*.java')
            sfs.exclude('reportlab/**/Makefile.*')
        else:
            if buildDir:
                sfs = SuperFileSet(fs)
                fs = superglob.FileSet(buildDir)
                fs.include('reportlab/docs/*.pdf')
                sfs.append(fs)
        return sfs

    def rlextraDistro(self,srcDir,full=1):
        fs = FileSet(srcDir)
        fs.include('rlextra/**/*')
        fs.exclude('rlextra/**/CVS/*')
        fs.exclude('rlextra/**/.svn/**/*')
        fs.exclude('rlextra/**/PROJECT_CVS_TAG')
        fs.exclude('rlextra/**/filelist.txt')       #if we need these wel'll make our own I suppose
        fs.exclude('rlextra/changeforms/**/*')
        fs.exclude('rlextra/delphi/**/*')
        fs.exclude('rlextra/designdocs/**/*')
        fs.exclude('rlextra/distro/**/*')
        fs.exclude('rlextra/dummydata/**/*')
        fs.exclude('rlextra/rlj/**/*')
        fs.exclude('rlextra/xmlforms/**/*')
        fs.exclude('rlextra/**/exe_stub*.*')
        fs.exclude('rlextra/**/do_freeze*.*')
        fs.exclude('rlextra/**/exemain*.*')
        fs.exclude('rlextra/rml2pdf/wordfilter/**/*')
        sfs = SuperFileSet(fs)
        if not full:
            sfs.exclude('rlextra/**/00readme.txt')
            sfs.exclude('rlextra/**/00README.txt')
            sfs.exclude('rlextra/**/*.bat')
            sfs.exclude('rlextra/bindings/**/*')
            sfs.exclude('rlextra/demos/**/*')
            sfs.exclude('rlextra/examples/**/*')
            sfs.exclude('rlextra/docs/**/*')
            sfs.exclude('rlextra/test/**/*')
            sfs.exclude('rlextra/tools/**/*')
            sfs.exclude('rlextra/utils/installer/**/*')
            sfs.exclude('rlextra/utils/tproj/**/*')
            sfs.exclude('rlextra/distro/**/*')
            sfs.exclude('rlextra/ers/examples/**/*')
            sfs.exclude('rlextra/pageCatcher/demos/**/*')
            sfs.exclude('rlextra/radxml/samples/**/*')
            sfs.exclude('rlextra/rml2pdf/demos/**/*')
            sfs.exclude('rlextra/rml2pdf/doc/**/*')
            sfs.exclude('rlextra/preppy/test/**/*')
            sfs.exclude('rlextra/preppy/flintstone*')
            sfs.exclude('rlextra/preppy/*.html')
            sfs.exclude('rlextra/preppy/*.prep')
            sfs.exclude('rlextra/**/README')
            sfs.exclude('rlextra/**/VERSION')
            sfs.exclude('rlextra/**/TODO')
            sfs.exclude('rlextra/**/changes')
            sfs.exclude('rlextra/graphics/barcode/**/*')
            sfs.exclude('rlextra/graphics/doc/**/*')
            sfs.exclude('rlextra/graphics/webcharts/**/*')
            sfs.exclude('rlextra/forms/test/**/*')
            sfs.exclude('rlextra/forms/examples/**/*')
            sfs.exclude('rlextra/forms/doc/**/*')
            sfs.exclude('rlextra/java/**/*')
            sfs.exclude('rlextra/pageCatcher/**/*.bmp')
            sfs.exclude('rlextra/pageCatcher/**/*.pdf')
            sfs.exclude('rlextra/pageCatcher/**/*.html')
            sfs.exclude('rlextra/pageCatcher/**/*.rml')
            sfs.exclude('rlextra/pageCatcher/test/**/*')
            sfs.exclude('rlextra/rml2pdf/test/**/*')
            sfs.exclude('rlextra/security/**/*')
            sfs.exclude('rlextra/test/**/*')
        return sfs

def _top_info(dir,R):
    for d in os.listdir(dir):
        D = path_join(dir,d)
        if isDir(D):
            if isDir(path_join(D,'.svn')):
                try:
                    R('URL=%(URL)s\n\tRevision=%(Revision)s\n\tLast Changed Rev=%(Last Changed Rev)s\n\tLast Changed Date=%(Last Changed Date)s\n\tLast Changed Author=%(Last Changed Author)s' % svn_info(D))
                except:
                    pass
            else:
                _top_info(D,R) #recur down one level

class ZipAppDef(StandardDistros):
    pfb = 1
    def projectDistro(self,projectPath):
        raise ValueError('Abstract method called')

    def copy_project(self, project, stgdir, app):
        print 'Copying %s files' % project
        FS = self.projectDistro(path_join(stgdir,project))
        FS.copyFiles(app)

    def fetch_project(self, project, workDir, stgdir, target=None):
            print 'Fetching %s from repository' % project
            svn_update(stgdir, findSVNURL(workDir), target=target or project)

    def __init__(self,
                EXPLODE_PATTERNS = [],      #'/' separated super glob style patterns for relative files
                moduleName = '',            #the module name containing the WebApp class
                appClassName = '',      #the name of the WebApp class
                cgiName = '',           #the default name of the cgi script
                appName = '',               #default base name of the target folder
                EARLY_ACTIONS = [],
                STD_ACTIONS = [
                    'self.make_dirs({{RAW}}"{{APPDIR}}")',
                    'self.make_webapp_cgi("{{APPNAME}}",{{RAW}}"{{APPDIR}}","{{MODULENAME}}","{{APPCLASSNAME}}","{{CGINAME}}",{{RAW}}"{{CGIDIR}}",{{RAW}}"{{LOGSDIR}}",{{RAW}}"{{OUTDIR}}")''',
                    'self.copyto({{RAW}}"{{APPDIR}}",(self._fn,"{{IZIPNAME}}"))',
                    'self.explodefilesrelative({{RAW}}"{{APPDIR}}",P=meta_data["filesrelative"])',
                    'self.removefrominstalledzip(os.path.join({{RAW}}"{{APPDIR}}","{{IZIPNAME}}"),P=meta_data["filesrelative"])',
                    'self.checkdirs("{{LOGSDIR}}","{{OUTDIR}}")',
                    ],
                LATE_ACTIONS = [],
                EXTRA_VALUES = [],
                EXTRA_META_DATA = {},
                EXTRA_SUBSTITUTABLES = [],
                EXTRA_APP_KEYWORD_ARG_SPECS = [],
                **kw
                ):
        for k,v in locals().items():
            if k not in ('self','kw'): setattr(self,k,v)
        self.__dict__.update(kw)

    def usage(self, msg='',exit=-1):
        import os
        msg = msg and [msg] or []
        msg += [
        'Use %s [options]' % os.path.basename(sys.argv[0]),
        '\toptions',
        '\t--help    give this help and exit',
        '\t--nosvn   don\'t update the SVN staging area',
        ]
        errorMsg('\n'.join(msg),exit)

    def _get_meta_data(self,**kw):
        meta_data = {
            'substitutables': ('actions','app_keyword_arg_specs')+tuple(self.EXTRA_SUBSTITUTABLES),
            'predefines': {'APPNAME':self.appName,'APPCLASSNAME':self.appClassName,'MODULENAME':self.moduleName},
            'values':[
                ('PYTHONEXE', 'sys.executable', 'python executable that should be used'),
                ('WWW_UID', '"nobody"', 'user name for permissions etc'),
                ('WWW_GID', '"nobody"', 'user name for permissions etc'),
                ('WEBSITE', 'os.path.join(os.getcwd())', 'here htdocs, cgi-bin etc live'),
                ('HTDOCSDIR', 'os.path.join({{RAW}}"{{WEBSITE}}","htdocs")', 'Where html documents live'),
                ('CGIDIR', 'os.path.join({{RAW}}"{{WEBSITE}}","cgi-bin")', 'Where cgi files live'),
                ('CGINAME', '"%s"' % self.cgiName,"Name of the CGI script"),
                ('APPDIR', 'os.path.join({{RAW}}"{{WEBSITE}}","{{APPNAME}}")','Application folder'),
                ('LOGSDIR', 'os.path.join({{RAW}}"{{WEBSITE}}","logs")','Where logging folders live (none for no logging)\n'),
                ('OUTDIR', 'os.path.join({{RAW}}"{{WEBSITE}}","pdfout")','Where output documents may be written'),
                ('HTTP_OUTDIR', '"/pdfout"', 'HTTP name of the output folder' ),
                ]+self.EXTRA_VALUES,
            'filesrelative': self.EXPLODE_PATTERNS, #files to be expanded relative (may include glob patterns)
            'actions': self.EARLY_ACTIONS + self.STD_ACTIONS + self.LATE_ACTIONS,
            'app_keyword_arg_specs': self.EXTRA_APP_KEYWORD_ARG_SPECS,
            }
        meta_data.update(self.EXTRA_META_DATA)
        meta_data.update(kw)
        return meta_data

    def build(self):
        AP = ArgProcessor(sys.argv)
        help = AP.getFlag('--help')
        nosvn = AP.getFlag('--nosvn')
        aversion = AP.getFlag('--version',1,None)
        I = AP.invalidFlags(())
        if I: self.usage('Invalid options %s' % (', '.join(I)))
        if help: self.usage(exit=0)
        workDir = chdir(path_dirname(sys.argv[0]))
        project = getattr(self,'project',None)
        if not (project and aversion):
            project, version = open('version.txt','r').readlines()[0].split()[0:2]
        if aversion: version = aversion
        timeStamp = time.strftime('%Y%m%d%H%M%S',time.gmtime())
        project_version = '%s-%s-%s' % (project,version,timeStamp)
        app = get_rl_tempdir(os.path.join(project+'_buildarea',project))
        chdir(workDir)
        print  'Removing build dir'
        kill(app)

        print 'Making build dir'
        os.makedirs(app)

        stgdir = project
        stgdir = stagingArea(stgdir)

        if not nosvn:
            self.fetch_project(project, workDir, stgdir)

            print 'Fetching reportlab & rlextra from repository'
            fetchRL(stgdir,rla=0,rlx='full',report=lambda x,w=sys.stdout.write: w('\t%s\n'%x),pfb=self.pfb)

        #obtain revisions etc for toplevel staged
        _ = []
        _top_info(stgdir,_.append)

        #store marshalled metadata
        f = open(path_join(app,'meta_data.mar'),'wb')
        import marshal
        marshal.dump(self._get_meta_data(project_version=project_version,top_info='\n\n'.join(_)),f)
        f.close()


        self.copy_project(project,stgdir,app)

        print 'copying reportlab'
        FS = self.reportlabDistro(stgdir,full=0)
        FS.copyFiles(app)

        print 'copying rlextra'
        FS = self.rlextraDistro(stgdir,full=0)
        FS.copyFiles(app)

        #assembly should now be complete we build everything into an installer
        from rlextra.utils.appmonitor import AppMonitor
        FS = FileSet(app)
        FS.include('**/*')
        m = AppMonitor(dirname=app,id=project,fileList=FS.getFileNames())
        m._force_ssmName = 0
        m.compileApp()
        m.writeSignedTargetManifest()
        zip = path_join(workDir,'%s.zip' % project_version)
        print 'Creating application archive',zip
        m.makeTargetArchive(zip)

def _substitute_values(s,v,known,V):
    if type(v)!=type(''): v = str(v)
    known[s] = v
    r = '{{%s}}' % s
    for i in xrange(len(V)):
        try:
            t0,t1,t2 = list(V[i])
            t1 = t1.replace(r,v)
            t2 = t2.replace(r,v)
            V[i] = (t0,t1,t2)
        except:
            print 'ERROR in substitutioni of r=%s into V=%s'%(str(r), str(V))
            import traceback
            traceback.print_exc()

from manifester import FilenameClassifier
_classifier_mode = FilenameClassifier().mode
del FilenameClassifier

class ArgProcessor:
    def __init__(self,argv,startAt=1):
        self.argv = argv[startAt:]
    def getFlag(self,name,nextVal=0,default=None):
        r = name in self.argv
        if nextVal and not r:
            name += '='
            for x in self.argv:
                if x.find(name)==0:
                    name = x
                    r = '='.join(name.split('=')[1:])
                    nextVal = 0
                    break
        if r:
            x = self.argv.index(name)
            del self.argv[x]
            if nextVal:
                try:
                    r = self.argv[x]
                    del self.argv[x]
                except:
                    r = (nextVal>>1)&1
        else:
            r = default
        return r

    def invalidFlags(self,allowed):
        I = []  #invalid flags
        for a in self.argv[1:]:
            if a[0]=='-':
                if a[:2]=='--' and a.find('=')>0: a = a.split('=')[0]
            if a not in allowed: I.append(a)
        return I

def _realpath(x):
    try:
        x = os.path.realpath(x)
    except:
        pass
    return os.path.abspath(os.path.normcase(os.path.normpath(x)))

def _relativeLocation(basedir,target):
    basedir = _realpath(basedir)
    target = _realpath(target)
    BL = basedir.split(os.sep)
    TL = target.split(os.sep)
    i = 0
    nBL = len(BL)
    nTL = len(TL)
    n = min(nBL,nTL)
    while i<n and BL[i]==TL[i]: i += 1
    return os.sep.join((nBL-i)*['..']+TL[i:])

def _rawChar():
    return os.sep=='\\' and 'r' or ''

def _targetnorm(target,dn):
    if target and not os.path.isabs(target):
        target = os.path.normpath(os.path.join(dn,target))
    return target

def _src2code(src,target_pyc):
    #code ripped from imputil
    import imp, struct, marshal, __builtin__
    code = __builtin__.compile(src,target_pyc,'exec')
    f = []
    f.append(imp.get_magic())
    f.append(struct.pack('<I', long(time.time())))
    f.append(marshal.dumps(code))
    return ''.join(f)

class _ZAA_Importer:
    '''An importer object intended to allow our dynamic things to be imported during the install'''
    def __init__(self):
        self.known = {}

    def add(self, n, c):
        n = os.path.splitext(n)[0].replace(os.sep,'.').replace('/','.')
        self.known[n] = c

    def find_module(self,fullname,path=None):
        try:
            self._get_code(fullname)
            return self
        except:
            return None

    def _get_code(self,fullname):
        try:
            return self.known[fullname]
        except KeyError:
            raise ImportError('Unknown module %s'%fullname)

    def load_module(self, fullname):
        import imp, marshal
        code = self._get_code(fullname)
        if code[0:4]!=imp.get_magic():
            raise ValueError('Bad magic number in zip entry "%s"' % script)
        code = marshal.loads(code[8:])
        mod = imp.new_module(fullname)
        sys.modules[fullname] = mod
        mod.__file__ = "<%s>" % self.__class__.__name__
        mod.__loader__ = self
        mod.__path__ = []
        exec code in mod.__dict__
        return mod

class ZipAppArchive:
    '''Encapsulate a zip archive for installable package uses'''
    def __init__(self,fileName):
        import zipimport
        self._ifn = self._fn = os.path.normcase(os.path.normpath(os.path.abspath(fileName)))
        zi = self._zi = zipimport.zipimporter(self._fn)
        self._files = map(lambda x,n=len(self._fn): x[:n], zi._files)
        self._unremoved = []
        self._unremovedD = []
        self._retained = []
        self._precious = []
        self._logfile = None
        self._dynamic_entries = []
        self._importer = _ZAA_Importer()

    def get_data(self,name):
        '''this should be a relative name'''
        return self._zi.get_data(os.path.join(self._fn,name))

    def _add_unremoved(self,d):
        unr = self._unremoved
        d = os.path.abspath(d)
        if isFile(d):
            unr.append(d)
        else:
            for _ in map(os.path.normcase,listAllFiles(d,rel=0)):
                if _ not in unr:
                    unr.append(_)
            und = self._unremovedD
            for _ in map(os.path.normcase,listAllDirs(d,rel=0)):
                if _ not in und:
                    und.append(_)

    def make_dirs(self, *dirs):
        precious = self._precious
        for d in dirs:
            if d==os.sep: raise ValueError("Not allowed to use file %s" % d)
            if isDir(d) or isFile(d):
                nv = self._overwrite and 'y' or self.yesnoquery('Path %s exists! OK to remove [NO]? ' % d )
                if nv.lower() in ['y','yes']:
                    if precious:
                        self._add_unremoved(d)
                    else:
                        kill(d)
                        os.mkdir(d)
                else:
                    if isDir(d):
                        nv = self.yesnoquery('Continue with installation [N]? ')
                    else:
                        nv = 'no'
                    if nv.lower() in ['y','yes']:
                        self._add_unremoved(d)
                        return
                    print >>sys.stderr, 'Cannot continue please back up "%s" and retry' % d
                    os._exit(0)
            else:
                os.makedirs(d)

    def copyto(self,dst,*files):
        if not isDir(dst): self.make_dirs(dst)
        for f in files:
            if type(f) is tuple:
                shutil.copy(f[0],dst)
                fn = path_join(dst,f[1])
                rename(path_join(dst,path_basename(f[0])),fn)
                if f[0]==self._fn:
                    self._ifn = fn
            else:
                shutil.copy(f,dst)
                fn = f
            try:
                self._unremoved.remove(os.path.normcase(fn))
            except:
                pass
            self._log('Installed File      "%s"' % fn)

    def explodefilesrelative(self,dst, P=[], N=[]):
        '''copy things glob matching P[i] to dst''' 
        import operator
        if not isDir(dst): self.make_dirs(dst)
        filt = lambda x,_files=self._files: fnmatch.filter(_files,x)
        F = map(lambda x: x.replace(os.sep,'/'),reduce(operator.add,[filt(p) for p in N],[]))
        F = [f for f in map(lambda x: x.replace(os.sep,'/'),reduce(operator.add,[filt(p) for p in P],[])) if f not in F]
        for f in F:
            m = _classifier_mode(f)
            d = self.get_data(f)
            if m=='t': d = d.replace('\r\n','\n').replace('\r','\n')
            self.writefile(os.path.join(dst,f), m, d)

    def removefrominstalledzip(self,installedzipfn,P=[],N=[]):
        '''remove things that match P, but not if they match N'''
        from zipfile import ZipFile
        import operator
        oldname=installedzipfn+'.old'
        rename(installedzipfn,oldname)
        old = ZipFile(oldname)
        filt = lambda x,_files=self._files: fnmatch.filter(_files,x)
        F = map(lambda x: x.replace(os.sep,'/'),reduce(operator.add,[filt(p) for p in N],[]))
        F = [f for f in map(lambda x: x.replace(os.sep,'/'),reduce(operator.add,[filt(p) for p in P],[])) if f not in F]
        new = ZipFile(installedzipfn,'w')
        while self._dynamic_entries:
            name, entry = self._dynamic_entries.pop(0)
            new.writestr(name,entry)
        for zi in old.infolist():
            fn = zi.filename
            if fn not in F:
                new.writestr(zi,old.read(fn))
        old.close()
        new.close()
        kill(oldname)

    def writefile(self,dst,mode,data):
        try:
            os.makedirs(os.path.dirname(dst))
            self._log('Installed Directory "%s"' % fn)
        except:
            pass
        try:
            self._unremoved.remove(os.path.normcase(os.path.abspath(dst)))
        except:
            pass
        if isFile(dst):
            f = open(dst,'r'+ (('b' in mode) and 'b' or 't'))
            odata = f.read()
            f.close()
            if odata==data:
                self._retained.append(dst)
                return
            nv = self._overwrite and 'y' or self.yesnoquery('File %s already exists OK to replace [NO]? ' % dst)
            if nv.lower() not in ['y','yes']:
                nv = self.yesnoquery('Continue with installation [NO]? ')
                if nv.lower() in ['y', 'yes']: return
                os._exit(0)
        if 'w' not in mode: mode = 'w'+mode
        f = open(dst,mode)
        f.write(data)
        f.close()
        self._log('Installed File      "%s"' % dst)

    def make_webapp_cgi(self,appName,appDir,appModuleName,appClassName,cgiName,cgiDir,logsDir,outDir):
        '''this will only be called when we have all values setup in self.'''
        known = self._known.copy()
        CGI2APPDIR = self.relativeLocation(cgiDir,appDir)
        APP2OUTDIR = self.relativeLocation(appDir,outDir)
        if logsDir.lower()=='none':
            LOGDIR = 'None'
        else:
            LOGDIR = '''os.path.abspath(%s'%s')''' % (self._known['RAW'],self.relativeLocation(appDir,logsDir))

        app_keyword_arg_specs = self._meta_data.get('app_keyword_arg_specs',[])
        app_keyword_arg_specs = app_keyword_arg_specs and ('\n\t'+(',\n\t'.join(app_keyword_arg_specs))+',') or ''
        known.update(locals())
        #now do substitution
        text = '''#!%(PYTHONEXE)s -u
import os, sys
os.chdir(%(RAW)s'%(CGI2APPDIR)s')
sys.path.insert(0,os.path.dirname(os.getcwd()))
sys.path.insert(0,os.getcwd())
sys.path.insert(0,os.path.join(os.getcwd(),%(RAW)s"%(IZIPNAME)s"))
from %(appModuleName)s import %(appClassName)s
from rlextra.ers.apploggers import OptionalFileAppLogger
%(appClassName)s(
    OUTDIR=os.path.abspath(os.path.join(%(RAW)s'%(APP2OUTDIR)s','%(APPNAME)s')),
    HTTP_OUTDIR='%(HTTP_OUTDIR)s/%(APPNAME)s',
    logger = OptionalFileAppLogger(%(LOGDIR)s),%(app_keyword_arg_specs)s
    ).handleCgi()
''' % known
        self.make_script(text,cgiDir,cgiName)

    def make_script(self,text,scriptDir,scriptName):
        scriptPath = os.path.join(scriptDir,scriptName)
        self.writefile(scriptPath,"t",text)
        self.chmod(0755,scriptPath)

    def make_config(self,pyc,**kw):
        code = []
        for k,v in kw.items():
            code.append('%s = %s' % (k,repr(v)))
        code = _src2code('\n'.join(code)+'\n',pyc)
        self._dynamic_entries.append((pyc, code))
        self._importer.add(pyc,code)

    def yesnoquery(self,prompt):
        yn = getattr(self,'_yesno',0)
        if not yn: return raw_input(prompt)
        return yn<0 and 'n' or 'y'

    def showMessage(self,msg, dolog=1):
        print msg
        if dolog: self._log(msg.strip())

    def runScriptFromFS(self,script,question='',workDir=None,argv=None):
        '''ask whether an existing script should be executed at install time
            if the answer is yes then execute the script
        '''
        if question and self.yesnoquery(question+'[NO]?').lower() not in ('y','yes'): return
        omods = sys.modules
        sys.modules = omods.copy()
        cwd = os.getcwd()
        oargv = sys.argv[:]
        try:
            script = os.path.abspath(script)
            import imputil
            if not imputil._os_stat: imputil._os_bootstrap()
            if script[-4:] in ('.pyc','.pyo'):
                py = script[:-4]+'.py'
                if isFile(py):
                    code = imputil.py_suffix_importer(py,os.stat(py),None)[1]
                else:
                    import imp, marshal
                    f = open(script, 'rb')
                    assert f.read(4)==imp.get_magic(), 'Bad magic number in file "%s"' % script
                    t = f.read(4)   #time stamp bytes
                    code = marshal.load(f)
                    f.close()
            elif script.endswith('.py'):
                code = imputil._compile(script,imputil._timestamp(script))
            else:
                raise ValueError('Don\'t know how to execute script "%s"' % script)
            if workDir: os.chdir(workDir)
            D = {'__name__': '__main__', '__file__': script}
            if self._fn not in sys.path: sys.path.insert(0,self._fn)
            sys.argv[0] = script
            if self._importer.known: sys.meta_path.insert(0,self._importer)
            exec code in D
            if self._importer.known:
                try:
                    sys.meta_path.remove(self._importer)
                except:
                    pass
        finally:
            if workDir: os.chdir(cwd)
            sys.modules = omods
            sys.argv = oargv

    def runScriptFromZip(self,script,question='',workDir=None):
        '''run a script from the zip file'''
        if question and self.yesnoquery(question+'[NO]?').lower() not in ('y','yes'): return
        omods = sys.modules
        sys.modules = omods.copy()
        cwd = os.getcwd()
        try:
            oargv = sys.argv[:]
            a0 = sys.argv[0] = os.path.join(self._fn,script)
            if workDir: os.chdir(workDir)
            code = self.get_data(script)
            import imp, marshal, __builtin__
            if script[:-3]=='.py':
                try:
                    code = __builtin__.compile(code,script,'exec')
                except Exception, e:
                    args = list(e.args)
                    if args: args[0] = 'Failed to compile zip entry "%s"\n%s' % (script,args[0])
                    raise e.__class__(*args),None,sys.exc_info()[3]
            else:
                if code[0:4]!=imp.get_magic():
                    raise ValueError('Bad magic number in zip entry "%s"' % script)
                code = marshal.loads(code[8:])
            D = {'__name__': '__main__', '__file__': a0}
            if self._fn not in sys.path: sys.path.insert(0,self._fn)
            exec code in D
        finally:
            if workDir: os.chdir(cwd)
            sys.modules = omods
            sys.argv = oargv

    def makeStub(self,entryName,target_pyc=None,target_py=None,zdn=None,ifn=None):
        if not ifn: ifn = self._ifn
        zdn = zdn or os.path.dirname(ifn)
        target_py = _targetnorm(target_py,zdn)
        target_pyc = _targetnorm(target_pyc,zdn)
        src = '\n'.join([
            '''#!%s''' % sys.executable,
            '''import sys, os''',
            '''zfn = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0]) or os.getcwd()),%s"%s")''' % (_rawChar(),self.relativeLocation(os.path.dirname(target_py or target_pyc),os.path.join(zdn,os.path.basename(ifn)))),
            '''sys.path.insert(0,zfn)''',
            '''from rlextra.utils.zipapp import ZipAppArchive''',
            '''ZipAppArchive(zfn).runScriptFromZip("%s")''' % entryName,
            ])+'\n'

        if target_py:
            self.writefile(target_py,'t',src)
            self.chmod(0755,target_py)

        if target_pyc:
            self.writefile(target_pyc,'b',_src2code(src,target_pyc))
            self.chmod(0755,target_pyc)

    def relativeLocation(self,basedir,target):
        return _relativeLocation(basedir,target)

    def _log(self,msg):
        if self._logfile: print >>self._logfile, msg

    def _cp_get(self,s,v):
        known = {}
        for k,x in self._known.items():
            known[k.lower()] = x
        for sect in (self._meta_data['project_version'].split('-')[0],'zipapp'):
            try:
                x = sect,s,self._cp.get(sect,s.lower(),0,known)
                return self._cp.get(sect,s.lower(),0,known)
            except:
                pass
        return v

    def _get_meta_data(self):
        import marshal
        return marshal.loads(self.get_data('meta_data.mar'))

    def install(self):
        self._logfilename = path_join(get_rl_tempdir('installs'),os.path.splitext(path_basename(self._fn))[0]+'.install.log')
        self._logfile = open(self._logfilename,'w')
        from rlextra.utils.cgisupport import TimeStamp
        self._log('Install carried out at %s GMT' % TimeStamp().timeStamp)
        self.showMessage("*NB* absolute path names should be entered, but will be relativized\nbefore use, so all can be moved bodily together!\n",dolog=0)
        AP = ArgProcessor(sys.argv)
        yesno = AP.getFlag('--yes')
        if AP.getFlag('--no'): yesno = -1
        self._yesno = yesno
        config = AP.getFlag('--config',1,None)
        if not config:
            HOME = os.environ.get('HOME',None)
            if HOME: config = os.path.join(HOME,'.zipapp.rc')
        self._meta_data = meta_data = self._get_meta_data()
        predefines = meta_data['predefines']
        if config and isFile(config):
            from ConfigParser import SafeConfigParser
            cp = SafeConfigParser(predefines)
            cp.read(config)
        else:
            cp = None
        self._cp = cp
        self._known = known = {}
        _ = ('y','yes','1','true')
        self._overwrite = AP.getFlag('--overwrite') or self._cp_get('overwrite','n').lower() in _
        forceInstall = AP.getFlag('--overwrite') or self._cp_get('overwrite','n').lower() in _
        goodConfig = AP.getFlag('--goodconfig') or self._cp_get('goodconfig','n').lower() in _

        V =  meta_data['values']
        zip = os.path.basename(self._fn)
        izip = os.path.splitext(zip)
        izip = '-'.join(izip[0].split('-')[:-2])+izip[1]
        _substitute_values('ZIPNAME',zip,known,V)
        _substitute_values('IZIPNAME',izip,known,V)
        _substitute_values('RAW',_rawChar(),known,V)
        for k,v in predefines.items():
            _substitute_values(k,v,known,V)
        NV = []
        for s,v,desc in V:
            nv = AP.getFlag('--%s' % s.lower(),1,None)
            if nv is not None:
                _substitute_values(s,nv,known,V)
                continue
            else:
                nv = self._cp_get(s,v)
                if nv is not v:
                    if goodConfig:
                        _substitute_values(s,nv,known,V)
                        continue
                    else:
                        v = '"%s"' % nv
            NV.append((s,v,desc))
        V = NV

        n = len(V)*10
        while V and n>0:
            s,v,desc = V.pop(0)
            n -= 1
            if s in known.keys(): continue
            if '{{' in v:
                V.insert(1,(s,v,desc))  #bubble down
                continue
            v = eval(v)
            if desc:
                nv = raw_input('\n%s\nEnter value of %s [default %s]? ' % (desc,s,v))
                if nv: v = nv
            _substitute_values(s,v,known,V)
        if V:
            print >> sys.stderr, 'These values are indeterminate', ' '.join(map(lambda x: x[0],V))
            os._exit(-1)
        for s in meta_data['substitutables']:
            S = meta_data[s]
            for k,v in known.items():
                r = '{{%s}}'%k
                if type(S) is type([]):
                    for x in xrange(len(S)):
                        S[x] = S[x].replace(r,v)
                else:
                    S = S.replace(r,v)
            meta_data[s] = S
        h40 = '#'*40
        self.showMessage('\n%s\nThe following settings are in effect' % h40)
        for k,v in known.items():
            self.showMessage('%s=%s' % (k,v))

        self._known = known

        self._log('\n%s\nActions' % h40)
        for s in meta_data['actions']:
            self._log(s)

        nv = forceInstall and 'y' or self.yesnoquery('Proceed with install [Y]? ')
        if nv and nv.lower() not in ['y','yes']:
            print >>sys.stderr, 'Install cancelled by user'
            os._exit(-1)
        self._log('\n%s\nInstalled Files and Directories' % h40)
        for s in meta_data['actions']:
            try:
                exec s
            except:
                self.showMessage('\n!!!!!!!!!!!!!! Error executing action\n"%s"' % s )
                raise
        self.special_test()
        self._log('\n%s\nThese files were retained as unchanged' % h40)
        for s in self._retained:
            self._log('Retained "%s"' % s)
        self.make_version()
        self.remove(*self._unremoved)
        self.removeD(*self._unremovedD)
        self.showMessage('File "%s"\ncontains a log of the installed files and directories!' % self._logfilename,dolog=0)

    def create_dirs(self,dirs=[], perms=0777):
        for x in dirs:
            try:
                os.makedirs(dir,perms)
            except:
                self.chmod(perms,dir)

    def special_test(self):
        try:
            import pyRXP
            v = float(pyRXP.version)
            if v<=1.02: self.showMessage('\n!!!!!!!!!!!!!! pyRXP.version=%.2f UPGRADE NOW to 1.03 or better!'% v)
        except:
            self.showMessage('\n!!!!!!!!!!!!!! Unable to import pyRXP')

    def chmod(self,mode,*path):
        for p in path:
            try:
                os.chmod(p,mode)
            except:
                self.showMessage('\n!!!!!!!!!!!!!! FAILED chmod 0%o %s'% (mode, p))

    def _unr_extend(self,unr):
        precious = self._precious
        fn = []
        for p in precious:
            fn.extend(fnmatch.filter(unr,p))
        for p in fn:
            unr.remove(p)
        return unr

    def remove(self,*path):
        unr = self._unremoved = self._unr_extend(self._unremoved)
        for p in path:
            for f in glob(p):
                fn = os.path.abspath(os.path.normpath(os.path.normcase(f)))
                if fn in unr:
                    try:
                        kill(fn)
                        self._log('Removed "%s"' % fn)
                        unr.remove(fn)
                    except:
                        self._log('\n!!!!!!!!!!!!!! FAILED remove "%s"'%fn)

    def removeD(self,*path):
        und = self._unremovedD = self._unr_extend(self._unremovedD)
        for p in path:
            for f in glob(p):
                fn = os.path.abspath(os.path.normpath(os.path.normcase(f)))
                if fn in und:
                    if not os.listdir(fn):
                        try:
                            os.rmdir(fn)
                            self._log('Removed "%s"' % fn)
                            und.remove(fn)
                        except:
                            self._log('\n!!!!!!!!!!!!!! FAILED remove "%s"'%fn)

    def make_version(self,appdir=None):
        if not appdir: appdir = self._known['APPDIR']
        project_version = self._meta_data['project_version']
        self.remove(path_join(appdir,'version.txt'))
        data = 'This folder contains %s installed at %4d%02d%02d%02d%02d%02d GMT' % ((project_version,)+tuple(time.gmtime()[:6]))
        self.writefile(path_join(appdir,'version.txt'),'t',data)

    def checkdirs(self,*path):
        for p in path:
            if isDir(p): continue
            if isFile(p):
                self.showMessage('\n!!!!!!!!!!!!!! FAILED checkdirs "%s" is a file!'% p)
            else:
                self.showMessage('\n!!!!!!!!!!!!!! FAILED checkdirs "%s" doesn\'t exist!'% p)

    def print_meta_data(self,full=0):
        from pprint import pprint
        I = self._get_meta_data().items()
        I.sort()
        i = 0
        for k,v in I:
            if full or k not in ('actions','substitutables','values', 'predefines', 'filesrelative'):
                if i: print
                else: i=1
                print k
                if k in ('project_version','top_info'):
                    print v
                else:
                    pprint(v)
