fix pylint error for python-support
[tools/python-support.git] / update-python-modules
1 #! /usr/bin/python
2 #
3 # copyright (c) 2006 Josselin Mouette <joss@debian.org>
4 # Licensed under the GNU Lesser General Public License, version 2.1
5 # See COPYING for details
6
7 # Everything prefixed by old_ is compatibility code with older versions
8 # Modules used to lie in /usr/{lib,share}/python-support/$package
9 # They now lie in /usr/{lib,share}/pyshared
10
11 import sys,os,shutil
12 from optparse import OptionParser
13 from subprocess import call
14 from py_compile import compile, PyCompileError
15 sys.path.append("/usr/share/python-support/private/")
16 import pysupport
17 from pysupport import py_supported,py_installed,py_oldversions
18
19 basepath='/usr/lib/pymodules'
20 sourcepath='/usr/share/python-support'
21 old_extensionpath='/usr/lib/python-support'
22 shared_path='/usr/share/pyshared'
23 shared_extensionpath='/usr/lib/pyshared'
24
25 parser = OptionParser(usage="usage: %prog [-v] [-c] package_directory [...]\n"+
26                             "       %prog [-v] [-c] package.dirs [...]\n"+
27                             "       %prog [-v] [-a|-f|-p]")
28 parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
29                   help="verbose output", default=False)
30 parser.add_option("-c", "--clean", action="store_true", dest="clean_mode",
31                   help="clean modules instead of compiling them",
32                   default=False)
33 parser.add_option("-a", "--rebuild-all", action="store_true",
34                   dest="rebuild_all", default=False,
35                   help="rebuild all private modules for a new default python version")
36 parser.add_option("-f", "--force-rebuild-all", action="store_true",
37                   dest="rebuild_everything", default=False,
38                   help="rebuild all modules, including public modules for all python versions")
39 parser.add_option("-p", "--post-install", action="store_true", dest="post_install",
40                   help="run post-installation operations, common to many packages",
41                   default=False)
42 parser.add_option("-b", "--bytecompile", action="store_true", dest="force_private",
43                   help="[deprecated] byte-compilation mode: only handle private modules",
44                   default=False)
45 parser.add_option("-i", "--install", action="store_true", dest="force_public",
46                   help="[deprecated] installation mode: only handle public modules",
47                   default=False)
48 (options, args) = parser.parse_args()
49
50 def debug(x):
51     if(options.verbose):
52         print x
53
54 def warning(x):
55     sys.stderr.write("WARNING: %s\n"%x)
56
57 def isect(l1,l2):
58     return [i for i in l1 if i in l2]
59
60 def concat(l1,l2):
61     return l1 + [i for i in l2 if i not in l1]
62
63
64 # Abstract class implementing the methods related to public modules
65 class _PublicList (list):
66     pyversions = py_supported
67     def install (self, versions):
68         versions = isect (self.pyversions, versions)
69         for filename in self:
70             version = None
71             rng = versions
72             try:
73                 if filename.startswith (shared_path+"/"):
74                     # New layout, module
75                     relname = filename[len(shared_path)+1:]
76                 elif filename.startswith (shared_extensionpath+"/python"):
77                     # New layout, extension
78                     [ version, relname ] = filename[len(shared_extensionpath)+1:].split("/", 1)
79                 elif filename.startswith (sourcepath+"/"):
80                     [ package, relname ] = filename[len(sourcepath)+1:].split("/",1)
81                 elif filename.startswith (old_extensionpath+"/"):
82                     [ package, version, relname ] = filename[len(old_extensionpath)+1:].split("/",2)
83                 else:
84                     raise ValueError
85             except ValueError:
86                 warning ("%s contains an invalid filename (%s)"%(self.name, filename))
87                 continue
88             if version:
89                 if version not in versions:
90                     continue
91                 rng = [version]
92             for pyversion in rng:
93                 destpath = os.path.join (basepath, pyversion, relname)
94                 try:
95                     os.makedirs(os.path.dirname(destpath))
96                 except OSError:
97                     pass
98                 if filename[-4:] not in ['.pyc', '.pyo']:
99                     debug("link "+destpath)
100                     # os.path.exists returns False for broken symbolic links
101                     if os.path.exists(destpath) or os.path.islink(destpath):
102                         if file!="__init__.py" or (os.path.exists(destpath) and os.path.getsize(destpath)):
103                             # The file is already here, probably from the previous version. 
104                             # No need to check for conflicts, dpkg catches them earlier now
105                             debug("overwrite "+destpath)
106                         else:
107                             debug("overwrite namespace "+destpath)
108                         if os.path.isdir(destpath):
109                             shutil.rmtree(destpath)
110                         else:
111                             os.remove(destpath)
112                     os.symlink(filename,destpath)
113
114
115 # Abstract class implementing the methods related to private modules
116 class _PrivateList (list):
117     pyversion = None
118     def bytecompile (self):
119         if self.pyversion:
120             debug("Byte-compilation of whole %s with python%s..."%(self.name,self.pyversion))
121             call(['/usr/bin/python'+self.pyversion, 
122                   os.path.join('/usr/lib','python'+self.pyversion,'py_compile.py')]
123                  + self)
124         else:
125             for filename in self:
126                 debug("compile "+filename+'c')
127                 try:
128                     # Note that compile doesn't raise PyCompileError by default
129                     compile(filename, doraise=True)
130                 except IOError, (errno, strerror):
131                     warning("I/O error while trying to byte-compile %s (%s): %s" % (filename, errno, strerror))
132                 except PyCompileError, inst:
133                     warning("compile error while trying to byte-compile %s: %s" % (filename, inst.msg))
134                 except:
135                     warning("unexpected error while trying to byte-compile %s: %s" % (filename, sys.exc_info()[0]))
136     def clean(self):
137         for filename in self:
138             for ext in ['c', 'o']:
139                 fullpath=filename+ext
140                 if os.path.exists(fullpath):
141                     debug("remove "+fullpath)
142                     os.remove(fullpath)
143
144
145 # Abstract class for PrivateFileList and SharedFileList
146 class _FileList(list):
147     def __init__ (self, path):
148         self.name = path
149         for line in file(path):
150             line = line.strip()
151             if (not line) or line.startswith('#'):
152                 continue
153             if line.startswith('/'):
154                 self.append(line)
155                 continue
156             line = [x.strip() for x in line.split('=',1)]
157             if len(line) != 2:
158                 warning("Parse error in %s"%path)
159                 continue
160             self.parse_option(*line)
161
162 # This class represents a file list as provided in the /usr/share/python-support/$package.public
163 # Useful for public modules and extensions
164 class SharedFileList(_FileList, _PublicList):
165     def parse_option (self, arg, value):
166         if arg=='pyversions':
167             self.pyversions = pysupport.version_list(value)
168         # Ignore unknown arguments for extensivity
169
170 # This class represents a file list as provided in the /usr/share/python-support/$package.private
171 # Useful for private modules
172 class PrivateFileList(_FileList, _PrivateList):
173     def parse_option (self, arg, value):
174         if arg=='pyversion':
175             self.pyversion = value
176
177 # This is a helper generator that goes through files of interest in a given directory
178 def allfiles(path, onlypy=False):
179     for root, dirs, files in os.walk(path):
180         for f in files:
181             if (onlypy and not f.endswith(".py")) or f== ".version":
182                 continue
183             yield os.path.join(root,f)
184         if not onlypy:
185             for d in dirs:
186                 d = os.path.join(root, d)
187                 if os.path.islink(d):
188                     yield d
189
190 # This class emulates the file listing as provided by /usr/share/python-support/$package.public
191 # with the deprecated layout /usr/{lib,share}/python-support/$package/
192 class SharedDirList(_PublicList):
193     def __init__ (self, path):
194         self.name = path
195         # Add all files to the file listing
196         self.extend(allfiles(path))
197         verfile=os.path.join(path,'.version')
198         extdir=path.replace(sourcepath,old_extensionpath,1)
199         if os.path.isfile(verfile):
200             # If we have a .version, use it
201             self.pyversions = pysupport.version_list(file(verfile).readline())
202         elif os.path.isdir(extdir):
203             # Try to obtain the list of supported versions
204             # from the extensions in /usr/lib
205             self.pyversions = isect(py_supported,os.listdir(extdir))
206         else:
207             # Otherwise, support all versions
208             pass
209
210         if os.path.isdir(extdir):
211             # Add the extensions to the file listing
212             for version in self.pyversions:
213                 self.extend(allfiles(os.path.join(extdir,version)))
214
215 # This class emulates the file listing as provided by /usr/share/python-support/$package.private
216 # with the deprecated layout /usr/share/python-support/$package.dirs
217 class PrivateDirList(_PrivateList):
218     def __init__ (self, path):
219         self.name = path
220         self.extend(allfiles(path, onlypy=True))
221         versionfile = os.path.join(path, ".pyversion")
222         if os.path.isfile(versionfile):
223             self.pyversion = file(versionfile).readline().strip()
224
225
226 class CachedFileList(dict):
227     def __getitem__ (self, name):
228         if name in self and dict.__getitem__(self, name) == None:
229             if name.startswith("/"):
230                 # The case of old-style private directories
231                 self[name] = PrivateDirList (name)
232             else:
233                 path = os.path.join (sourcepath, name)
234                 if name.endswith(".public"):
235                     self[name] = SharedFileList (path)
236                 elif name.endswith(".private"):
237                     self[name] = PrivateFileList (path)
238                 elif os.path.isdir(path):
239                     self[name] = SharedDirList (path)
240                 else:
241                     raise Exception("[Internal Error] I don't know what to do with this path: %s"%path)
242         return dict.__getitem__(self, name)
243
244
245 def bytecompile_all(py,path=None):
246     if not path:
247         path=os.path.join(basepath,py)
248     if not os.path.isdir(path):
249         return
250     debug("Byte-compilation of whole %s..."%path)
251     os.spawnl(os.P_WAIT, '/usr/bin/'+py, py,
252               os.path.join('/usr/lib/',py,'compileall.py'), '-q', path)
253
254 # A function to create the ".path" at the root of the installed directory
255 # Returns the list of affected directories
256 def create_dotpath(py):
257   path=os.path.join(basepath,py)
258   if not os.path.isdir(path):
259     return
260   pathfile=os.path.join(path,".path")
261   debug("Generation of %s..."%pathfile)
262   pathlist=[path]
263   ret=[]
264   for f in os.listdir(path):
265     f=os.path.join(path,f)
266     if f.endswith(".pth") and os.path.isfile(f):
267       for l in file(f):
268         l=l.rstrip('\n')
269         if l.startswith('import'):
270           # Do not ship lines starting with "import", they are executed! (complete WTF)
271           continue
272         pathlist.append(l)
273         l2=os.path.join(path,l)
274         pathlist.append(l2)
275         ret.append(l2)
276   fd=file(pathfile,"w")
277   fd.writelines([l+'\n' for l in pathlist])
278   fd.close()
279   return ret
280
281 def post_change_stuff(py):
282   # All the changes that need to be done after anything has changed
283   # in a /usr/lib/pymodules/pythonX.Y directory
284   # * Cleanup of all dangling symlinks that are left out after a package
285   #   is upgraded/removed.
286   # * The namespace packages are here because python doesn't consider a
287   #   directory to be able to contain packages if there is no __init__.py
288   #   file (yes, this is completely stupid).
289   # * The .path file must be created by concatenating all those .pth
290   #   files that extend sys.path (this also badly sucks).
291   # * Byte-compilation of all .py files that haven't already been
292   path=os.path.join(basepath,py)
293   if not os.path.isdir(path):
294     return
295   # First, remove any dangling symlinks.
296   # In the same loop, we find which directories may need a namespace package
297   dirhash={}
298   for dir, dirs, files in os.walk(path):
299     dirhash[dir]=False
300     files.sort() # We need the .py to appear before the .pyc
301     for f in files+dirs:
302       # We also examine dirs as some symlinks are dirs
303       abspath=os.path.join(dir,f)
304       islink=os.path.islink(abspath)
305       if islink:
306         if not os.path.exists(abspath):
307           # We refer to a file that was removed
308           debug("remove "+abspath)
309           os.remove(abspath)
310           continue
311         srcfile = os.readlink (abspath)
312         # Remove links left here after a change in the supported python versions for a package
313         removed = False
314         for package in public_packages:
315           if srcfile in public_packages[package]:
316             if py not in public_packages[package].pyversions:
317               debug("remove "+abspath)
318               os.remove(abspath)
319               removed = True
320             break
321         else:
322           # Remove files provided by packages that do not use python-support anymore
323           debug("remove "+abspath)
324           os.remove(abspath)
325           removed = True
326         if removed:
327           # Do not go further, the file was removed
328           continue
329       if f[-4:] in ['.pyc', '.pyo']:
330         if not os.path.exists(abspath[:-1]):
331           debug("remove "+abspath)
332           os.remove(abspath)
333           continue
334       elif f[-3:] in ['.py', '.so']:
335         if islink or f!='__init__.py':
336           # List the directory as maybe needing a namespace packages
337           d=dir
338           while dirhash.has_key(d) and not dirhash[d]:
339             dirhash[d]=True
340             d=os.path.dirname(d)
341     # Remove the directory if it is empty after our crazy removals
342     try:
343       os.removedirs(dir)
344     except OSError:
345       pass
346   dirhash[path]=False
347   # Then, find which directories belong in a .pth file
348   # These directories don't need a namespace package, so we
349   # set them to False in dirhash
350   for p in create_dotpath (py):
351     dirhash[p] = False
352   # Finally, create/remove namespace packages
353   for dir in dirhash:
354     initfile=os.path.join(dir,"__init__.py")
355     noinitfile=os.path.join(dir,".noinit")
356     if dirhash[dir] and not os.path.exists(noinitfile):
357       if not os.path.exists(initfile):
358         debug("create namespace "+initfile)
359         file(initfile,"w").close()
360     else:
361       for e in ['','c','o']:
362         if os.path.exists(initfile+e):
363           debug('remove namespace '+initfile+e)
364           os.remove(initfile+e)
365       try:
366         os.removedirs(dir)
367       except OSError:
368         pass
369   bytecompile_all(py)
370
371
372 # A helper function for older $package.dirs files
373 def dirlist_file(f):
374     return [ l.rstrip('\n') for l in file(f) if len(l)>1 ]
375
376 # End of function definitions - Start of the script itself
377
378 # Ensure that the umask is sane
379 os.umask(022)
380
381 # Read all modules listing
382 public_packages = CachedFileList()
383 private_packages = CachedFileList()
384 dirlisting = os.listdir(sourcepath)
385 for name in dirlisting:
386     path=os.path.join(sourcepath,name)
387     if name == "private":
388         continue
389     ext = name.split(".")[-1]
390     if os.path.isdir(path):
391         if ext in ["public", "private", "dirs"]:
392             # Presumably a bogus directory, see #528130
393             warning("%s is a directory"%name)
394         else:
395             public_packages[name] = None
396         continue
397     if not os.path.isfile(path):
398         # Ignore whatever is not a file, like dangling symlinks
399         continue
400     if ext == "public":
401         public_packages[name] = None
402     elif ext == "private":
403         private_packages[name] = None
404     elif ext == "dirs":
405         for dirname in dirlist_file (path):
406             private_packages[dirname] = None
407     # Just ignore all other files
408
409 # Parse arguments
410 do_public=[]
411 do_private=[]
412 for arg in args:
413     if arg.startswith(sourcepath):
414         arg = arg[len(sourcepath):].lstrip("/")
415     if arg.endswith(".dirs") and arg in dirlisting:
416         for dirname in dirlist_file(os.path.join(sourcepath, arg)):
417             do_private.append(private_packages[dirname])
418     elif arg in public_packages:
419         do_public.append(public_packages[arg])
420     elif arg in private_packages:
421         do_private.append(private_packages[arg])
422     else:
423         if options.clean_mode:
424             warning("%s does not exist.\n         Some bytecompiled files may be left behind."%arg)
425         else:
426             parser.error("%s is not a recognized python-support module."%arg)
427
428 # Check consistency options (although these ones should not exist anymore)
429 if do_private and options.force_public:
430     parser.error("Option -i cannot be used with a .private module file.")
431 if do_public and options.force_private:
432     parser.error("Option -b cannot be used with a .public module file.")
433
434 if options.rebuild_everything:
435     options.rebuild_all = True
436     for pyver in py_supported:
437         dir = os.path.join(basepath,pyver)
438         if os.path.isdir(dir):
439             shutil.rmtree(dir)
440
441 # Check for changes in installed python versions
442 need_postinstall = []
443 for pyver in py_oldversions+py_supported:
444     dir = os.path.join(basepath,pyver)
445     # Check for ".path" because sometimes the directory already exists 
446     # while the python version isn't installed, because of some .so's.
447     if pyver not in py_installed and os.path.isdir(dir):
448         debug("Removing obsolete directory %s..."%(dir))
449         shutil.rmtree(dir)
450     if pyver in py_installed and not os.path.isfile(os.path.join(dir,".path")):
451         need_postinstall.append(pyver)
452 if need_postinstall:
453     debug("Building all modules for %s..."%(" ".join(need_postinstall)))
454     for package in public_packages:
455         public_packages[package].install(need_postinstall)
456     for pyver in need_postinstall:
457         # Here we need to launch create_dotpath because otherwise we could
458         # end up without the .path file that is checked 6 lines earlier
459         create_dotpath(pyver)
460
461 if options.rebuild_all:
462     for package in private_packages:
463         private_packages[package].bytecompile()
464
465
466 # Now for the processing of what was handed on the command line
467 for package in do_private:
468     if not options.clean_mode:
469         package.bytecompile()
470     else:
471         package.clean()
472
473 need_dotpath = False
474 for package in do_public:
475     need_postinstall = concat (need_postinstall, isect(package.pyversions,py_installed))
476     if options.clean_mode:
477         continue
478     package.install(py_installed)
479     for f in package:
480         if f.endswith(".pth"):
481             need_dotpath = True
482
483 # Only do the funny and time-consuming things when the -p option is
484 # given, e.g when python-support is triggered.
485 if need_postinstall and 'DPKG_RUNNING_VERSION' in os.environ and not options.post_install:
486     ret = os.spawnlp(os.P_WAIT, 'dpkg-trigger', 'dpkg-trigger', '--no-await', 'pysupport')
487     if ret:
488         sys.stderr.write("ERROR: dpkg-trigger failed\n")
489         sys.exit(1)
490     if need_dotpath:
491         for py in need_postinstall:
492             create_dotpath (py)
493     need_postinstall = []
494
495 if options.post_install:
496     # The trigger has been activated; do it for all installed versions
497     need_postinstall = py_installed
498 if need_postinstall:
499     need_dotpath = False
500     for py in need_postinstall:
501         post_change_stuff(py)
502