![]() |
|
|
Generated: 8 Jan 2009 |
00001 #!/usr/bin/env python 00002 """ 00003 Script used to install files keeping track of the files that have 00004 been installed, so that at the next installation the file removed 00005 from the source directory will also be removed from the destination 00006 directory. 00007 The script provide also the "uninstall" functionality to remove all 00008 and only the files that it installed for the package. 00009 00010 Command line: 00011 00012 install.py [-x exclusion1 [-x exclusion2 ...]] [-l logfile] source1 [source2 ...] dest 00013 install.py -u [-l logfile] [dest1 ...] 00014 00015 @author: Marco Clemencic <marco.clemencic@cern.ch> 00016 """ 00017 # Needed to for the local copy of the function os.walk, introduced in Python 2.3 00018 # It must be removed when the support for Python 2.2 is dropped 00019 from __future__ import generators # should be at the first line to please Python 2.5 00020 _version = "$Id: install.py,v 1.15 2008/10/28 17:24:39 marcocle Exp $" 00021 00022 def main(): 00023 try: 00024 # optparse is available only since Python 2.3 00025 from optparse import OptionParser 00026 parser = OptionParser() 00027 parser.add_option("-x","--exclude",action="append", 00028 metavar="PATTERN", default = [], 00029 dest="exclusions", help="which files/directories to avoid to install") 00030 parser.add_option("-l","--log",action="store", 00031 dest="logfile", default="install.log", 00032 help="where to store the informations about installed files [default: %default]") 00033 parser.add_option("-d","--destname",action="store", 00034 dest="destname", default=None, 00035 help="name to use when installing the source into the destination directory [default: source name]") 00036 parser.add_option("-u","--uninstall",action="store_true", 00037 dest="uninstall", default=False, 00038 help="do uninstall") 00039 parser.add_option("-s","--symlink",action="store_true", 00040 dest="symlink", default=False, 00041 help="create symlinks instead of copy") 00042 #parser.add_option("-p","--permission",action="store", 00043 # metavar="PERM", 00044 # dest="permission", 00045 # help="modify the permission of the destination file (see 'man chown'). Unix only.") 00046 (opts,args) = parser.parse_args() 00047 except ImportError: 00048 # Old style option parsing 00049 # It must be removed when the support for Python 2.2 is dropped 00050 from getopt import getopt, GetoptError 00051 from sys import argv,exit 00052 class _DummyParserClass: 00053 def __init__(self): 00054 self.usage = "usage: install.py [options]" 00055 self.help = """options: 00056 -h, --help show this help message and exit 00057 -x PATTERN, --exclude=PATTERN 00058 which files/directories to avoid to install 00059 -l LOGFILE, --log=LOGFILE 00060 where to store the informations about installed files 00061 [default: install.log] 00062 -d DESTNAME, --destname=DESTNAME 00063 name to use when installing the source into the 00064 destination directory [default: source name] 00065 -u, --uninstall do uninstall 00066 -s, --symlink create symlinks instead of copy""" 00067 def error(self,msg=None): 00068 print self.usage + "\n" 00069 if not msg: 00070 msg = self.help 00071 print msg 00072 exit(1) 00073 parser = _DummyParserClass() 00074 try: 00075 optlist, args = getopt(argv[1:],"hx:l:d:us", 00076 ["help","exclude","log","destname","uninstall","symlink"]) 00077 except GetoptError: 00078 # print help information and exit: 00079 parser.error() 00080 # Dummy option class 00081 class _DummyOptionsClass: 00082 def __init__(self): 00083 # defaults 00084 self.exclusions = [] 00085 self.uninstall = False 00086 self.logfile = "install.log" 00087 self.destname = None 00088 self.symlink = False 00089 opts = _DummyOptionsClass() 00090 for opt,val in optlist: 00091 if opt in [ "-h", "--help" ]: 00092 parser.error() 00093 elif opt in [ "-x", "--exclude" ]: 00094 opts.exclusions.append(val) 00095 elif opt in [ "-l", "--log" ]: 00096 opts.logfile = val 00097 elif opt in [ "-d", "--destname" ]: 00098 opts.destname = val 00099 elif opt in [ "-u", "--uninstall" ]: 00100 opts.uninstall = True 00101 elif opt in [ "-s", "--symlink" ]: 00102 opts.symlink = True 00103 # options consistency check 00104 from pickle import dump,load 00105 from os.path import realpath 00106 if opts.uninstall: 00107 if opts.exclusions: 00108 parser.error("Exclusion list does not make sense for uninstall") 00109 opts.destination = args 00110 try: 00111 log = load(open(opts.logfile,"rb")) 00112 except: 00113 log = LogFile() 00114 uninstall(log,opts.destination,realpath(dirname(opts.logfile))) 00115 if log: 00116 dump(log,open(opts.logfile,"wb")) 00117 else: 00118 from os import remove 00119 try: 00120 remove(opts.logfile) 00121 except OSError, x: 00122 if x.errno != 2 : raise 00123 else : # install mode 00124 if len(args) < 2: 00125 parser.error("Specify at least one source and (only) one destination") 00126 opts.destination = args[-1] 00127 opts.sources = args[:-1] 00128 try: 00129 log = load(open(opts.logfile,"rb")) 00130 except: 00131 log = LogFile() 00132 if opts.symlink : 00133 if len(opts.sources) != 1: 00134 parser.error("no more that 2 args with --symlink") 00135 opts.destination, opts.destname = split(opts.destination) 00136 install(opts.sources,opts.destination, 00137 log,opts.exclusions,opts.destname, 00138 opts.symlink, realpath(dirname(opts.logfile))) 00139 dump(log,open(opts.logfile,"wb")) 00140 00141 from os import makedirs, listdir, rmdir 00142 from os.path import exists, isdir, getmtime, split, join, realpath, dirname 00143 00144 try: 00145 from os import walk 00146 except ImportError: 00147 def walk(top, topdown=True, onerror=None): 00148 """Copied from Python 2.3 os.py (see original file for copyright) 00149 This function has been introduced in Python 2.3, and this copy should 00150 be removed once the support for Python 2.2 is dropped. 00151 """ 00152 00153 from os.path import join, isdir, islink 00154 00155 # We may not have read permission for top, in which case we can't 00156 # get a list of the files the directory contains. os.path.walk 00157 # always suppressed the exception then, rather than blow up for a 00158 # minor reason when (say) a thousand readable directories are still 00159 # left to visit. That logic is copied here. 00160 try: 00161 # Note that listdir and error are globals in this module due 00162 # to earlier import-*. 00163 names = listdir(top) 00164 except error, err: 00165 if onerror is not None: 00166 onerror(err) 00167 return 00168 00169 dirs, nondirs = [], [] 00170 for name in names: 00171 if isdir(join(top, name)): 00172 dirs.append(name) 00173 else: 00174 nondirs.append(name) 00175 00176 if topdown: 00177 yield top, dirs, nondirs 00178 for name in dirs: 00179 path = join(top, name) 00180 if not islink(path): 00181 for x in walk(path, topdown, onerror): 00182 yield x 00183 if not topdown: 00184 yield top, dirs, nondirs 00185 00186 class LogFile: 00187 """ 00188 Class to incapsulate the logfile functionalities. 00189 """ 00190 def __init__(self): 00191 self._installed_files = {} 00192 00193 def get_dest(self,source): 00194 try: 00195 return self._installed_files[source] 00196 except KeyError: 00197 return None 00198 00199 def set_dest(self,source,dest): 00200 self._installed_files[source] = dest 00201 00202 def get_sources(self): 00203 return self._installed_files.keys() 00204 00205 def remove(self,source): 00206 try: 00207 del self._installed_files[source] 00208 except KeyError: 00209 pass 00210 00211 def __len__(self): 00212 return self._installed_files.__len__() 00213 00214 def filename_match(name,patterns,default=False): 00215 """ 00216 Check if the name is matched by any of the patterns in exclusions. 00217 """ 00218 from fnmatch import fnmatch 00219 for x in patterns: 00220 if fnmatch(name,x): 00221 return True 00222 return default 00223 00224 def expand_source_dir(source, destination, exclusions = [], 00225 destname = None, logdir = realpath(".")): 00226 """ 00227 Generate the list of copies. 00228 """ 00229 expansion = {} 00230 src_path,src_name = split(source) 00231 if destname: 00232 to_replace = source 00233 replacement = join(destination,destname) 00234 else: 00235 to_replace = src_path 00236 replacement = destination 00237 00238 for dirname, dirs, files in walk(source): 00239 if to_replace: 00240 dest_path=dirname.replace(to_replace,replacement) 00241 else: 00242 dest_path=join(destination,dirname) 00243 # remove excluded dirs from the list 00244 dirs[:] = [ d for d in dirs if not filename_match(d,exclusions) ] 00245 # loop over files 00246 for f in files: 00247 if filename_match(f,exclusions): continue 00248 key = getRelativePath(dest_path, join(dirname,f)) 00249 value = getRelativePath(logdir, join(dest_path,f)) 00250 expansion[key] = value 00251 return expansion 00252 00253 def remove(file, logdir): 00254 from os import remove 00255 from os.path import normpath, splitext, exists 00256 file = normpath(join(logdir, file)) 00257 try: 00258 print "Remove '%s'"%file 00259 remove(file) 00260 # For python files, remove the compiled versions too 00261 if splitext(file)[-1] == ".py": 00262 for c in ['c', 'o']: 00263 if exists(file + c): 00264 print "Remove '%s'" % (file+c) 00265 remove(file+c) 00266 file_path = split(file)[0] 00267 while file_path and (len(listdir(file_path)) == 0): 00268 print "Remove empty dir '%s'"%file_path 00269 rmdir(file_path) 00270 file_path = split(file_path)[0] 00271 except OSError, x: # ignore file-not-found errors 00272 if x.errno in [2, 13] : 00273 print "Previous removal ignored" 00274 else: 00275 raise 00276 00277 00278 def getCommonPath(dirname, filename): 00279 from os import sep 00280 from itertools import izip 00281 from os.path import splitdrive 00282 # if the 2 components are on different drives (windows) 00283 if splitdrive(dirname)[0] != splitdrive(filename)[0]: 00284 return None 00285 dirl = dirname.split(sep) 00286 filel = filename.split(sep) 00287 commpth = [] 00288 for d, f in izip(dirl, filel): 00289 if d == f : 00290 commpth.append(d) 00291 else : 00292 break 00293 commpth = sep.join(commpth) 00294 if not commpth: 00295 commpth = sep 00296 elif commpth[-1] != sep: 00297 commpth += sep 00298 return commpth 00299 00300 def getRelativePath(dirname, filename): 00301 """ calculate the relative path of filename with regards to dirname """ 00302 import os.path 00303 # Translate the filename to the realpath of the parent directory + basename 00304 filepath,basename = os.path.split(filename) 00305 filename = os.path.join(os.path.realpath(filepath),basename) 00306 # Get the absolute pathof the destination directory 00307 dirname = os.path.realpath(dirname) 00308 commonpath = getCommonPath(dirname, filename) 00309 # for windows if the 2 components are on different drives 00310 if not commonpath: 00311 return filename 00312 relname = filename[len(commonpath):] 00313 reldir = dirname[len(commonpath):] 00314 if reldir: 00315 relname = (os.path.pardir+os.path.sep)*len(reldir.split(os.path.sep)) \ 00316 + relname 00317 return relname 00318 00319 def update(src,dest,old_dest = None, syml = False, logdir = realpath(".")): 00320 from shutil import copy2 00321 from sys import platform 00322 from os.path import normpath 00323 if platform != "win32": 00324 from os import symlink 00325 realdest = normpath(join(logdir, dest)) 00326 dest_path = split(realdest)[0] 00327 realsrc = normpath(join(dest_path,src)) 00328 if (not exists(realdest)) or (getmtime(realsrc) > getmtime(realdest)): 00329 if not isdir(dest_path): 00330 print "Create dir '%s'"%(dest_path) 00331 makedirs(dest_path) 00332 # the destination file is missing or older than the source 00333 if syml and platform != "win32" : 00334 if exists(realdest): 00335 remove(realdest,logdir) 00336 print "Create Link to '%s' in '%s'"%(src,dest_path) 00337 symlink(src,realdest) 00338 else: 00339 print "Copy '%s' -> '%s'"%(src,realdest) 00340 copy2(realsrc,realdest) # do the copy (cp -p src dest) 00341 #if old_dest != dest: # the file was installed somewhere else 00342 # # remove the old destination 00343 # if old_dest is not None: 00344 # remove(old_dest,logdir) 00345 00346 def install(sources, destination, logfile, exclusions = [], 00347 destname = None, syml = False, logdir = realpath(".")): 00348 """ 00349 Copy sources to destination keeping track of what has been done in logfile. 00350 The destination must be a directory and sources are copied into it. 00351 If exclusions is not empty, the files matching one of its elements are not 00352 copied. 00353 """ 00354 for s in sources: 00355 src_path, src_name = split(s) 00356 if not exists(s): 00357 continue # silently ignore missing sources 00358 elif not isdir(s): # copy the file, without logging (?) 00359 if destname is None: 00360 dest = join(destination,src_name) 00361 else: 00362 dest = join(destination,destname) 00363 src = getRelativePath(destination,s) 00364 dest = getRelativePath(logdir,dest) 00365 old_dest = logfile.get_dest(src) 00366 update(src,dest,old_dest,syml,logdir) 00367 logfile.set_dest(src,dest) # update log 00368 else: # for directories 00369 # expand the content of the directory as a dictionary 00370 # mapping sources to destinations 00371 to_do = expand_source_dir(s,destination,exclusions,destname, logdir) 00372 src = getRelativePath(destination,s) 00373 last_done = logfile.get_dest(src) 00374 if last_done is None: last_done = {} 00375 for k in to_do: 00376 try: 00377 old_dest = last_done[k] 00378 del last_done[k] 00379 except KeyError: 00380 old_dest = None 00381 update(k,to_do[k],old_dest,syml,logdir) 00382 # remove files that were copied but are not anymore in the list 00383 for old_dest in last_done.values(): 00384 remove(old_dest,logdir) 00385 logfile.set_dest(src,to_do) # update log 00386 00387 def uninstall(logfile, destinations = [], logdir=realpath(".")): 00388 """ 00389 Remove copied files using logfile to know what to remove. 00390 If destinations is not empty, only the files/directories specified are 00391 removed. 00392 """ 00393 for s in logfile.get_sources(): 00394 dest = logfile.get_dest(s) 00395 if type(dest) is str: 00396 if filename_match(dest,destinations,default=True): 00397 remove(dest, logdir) 00398 logfile.remove(s) 00399 else: 00400 for subs in dest.keys(): 00401 subdest = dest[subs] 00402 if filename_match(subdest,destinations,default=True): 00403 remove(subdest,logdir) 00404 del dest[subs] 00405 if not dest: 00406 logfile.remove(s) 00407 00408 if __name__ == "__main__": 00409 main()