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
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
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/")
17 from pysupport import py_supported,py_installed,py_oldversions
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'
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",
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",
42 parser.add_option("-b", "--bytecompile", action="store_true", dest="force_private",
43 help="[deprecated] byte-compilation mode: only handle private modules",
45 parser.add_option("-i", "--install", action="store_true", dest="force_public",
46 help="[deprecated] installation mode: only handle public modules",
48 (options, args) = parser.parse_args()
55 sys.stderr.write("WARNING: %s\n"%x)
58 return [i for i in l1 if i in l2]
61 return l1 + [i for i in l2 if i not in l1]
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)
73 if filename.startswith (shared_path+"/"):
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)
86 warning ("%s contains an invalid filename (%s)"%(self.name, filename))
89 if version not in versions:
93 destpath = os.path.join (basepath, pyversion, relname)
95 os.makedirs(os.path.dirname(destpath))
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)
107 debug("overwrite namespace "+destpath)
108 if os.path.isdir(destpath):
109 shutil.rmtree(destpath)
112 os.symlink(filename,destpath)
115 # Abstract class implementing the methods related to private modules
116 class _PrivateList (list):
118 def bytecompile (self):
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')]
125 for filename in self:
126 debug("compile "+filename+'c')
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))
135 warning("unexpected error while trying to byte-compile %s: %s" % (filename, sys.exc_info()[0]))
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)
145 # Abstract class for PrivateFileList and SharedFileList
146 class _FileList(list):
147 def __init__ (self, path):
149 for line in file(path):
151 if (not line) or line.startswith('#'):
153 if line.startswith('/'):
156 line = [x.strip() for x in line.split('=',1)]
158 warning("Parse error in %s"%path)
160 self.parse_option(*line)
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
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):
175 self.pyversion = value
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):
181 if (onlypy and not f.endswith(".py")) or f== ".version":
183 yield os.path.join(root,f)
186 d = os.path.join(root, d)
187 if os.path.islink(d):
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):
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))
207 # Otherwise, support all versions
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)))
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):
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()
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)
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)
241 raise Exception("[Internal Error] I don't know what to do with this path: %s"%path)
242 return dict.__getitem__(self, name)
245 def bytecompile_all(py,path=None):
247 path=os.path.join(basepath,py)
248 if not os.path.isdir(path):
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)
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):
260 pathfile=os.path.join(path,".path")
261 debug("Generation of %s..."%pathfile)
264 for f in os.listdir(path):
265 f=os.path.join(path,f)
266 if f.endswith(".pth") and os.path.isfile(f):
269 if l.startswith('import'):
270 # Do not ship lines starting with "import", they are executed! (complete WTF)
273 l2=os.path.join(path,l)
276 fd=file(pathfile,"w")
277 fd.writelines([l+'\n' for l in pathlist])
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):
295 # First, remove any dangling symlinks.
296 # In the same loop, we find which directories may need a namespace package
298 for dir, dirs, files in os.walk(path):
300 files.sort() # We need the .py to appear before the .pyc
302 # We also examine dirs as some symlinks are dirs
303 abspath=os.path.join(dir,f)
304 islink=os.path.islink(abspath)
306 if not os.path.exists(abspath):
307 # We refer to a file that was removed
308 debug("remove "+abspath)
311 srcfile = os.readlink (abspath)
312 # Remove links left here after a change in the supported python versions for a package
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)
322 # Remove files provided by packages that do not use python-support anymore
323 debug("remove "+abspath)
327 # Do not go further, the file was removed
329 if f[-4:] in ['.pyc', '.pyo']:
330 if not os.path.exists(abspath[:-1]):
331 debug("remove "+abspath)
334 elif f[-3:] in ['.py', '.so']:
335 if islink or f!='__init__.py':
336 # List the directory as maybe needing a namespace packages
338 while dirhash.has_key(d) and not dirhash[d]:
341 # Remove the directory if it is empty after our crazy removals
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):
352 # Finally, create/remove namespace packages
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()
361 for e in ['','c','o']:
362 if os.path.exists(initfile+e):
363 debug('remove namespace '+initfile+e)
364 os.remove(initfile+e)
372 # A helper function for older $package.dirs files
374 return [ l.rstrip('\n') for l in file(f) if len(l)>1 ]
376 # End of function definitions - Start of the script itself
378 # Ensure that the umask is sane
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":
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)
395 public_packages[name] = None
397 if not os.path.isfile(path):
398 # Ignore whatever is not a file, like dangling symlinks
401 public_packages[name] = None
402 elif ext == "private":
403 private_packages[name] = None
405 for dirname in dirlist_file (path):
406 private_packages[dirname] = None
407 # Just ignore all other files
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])
423 if options.clean_mode:
424 warning("%s does not exist.\n Some bytecompiled files may be left behind."%arg)
426 parser.error("%s is not a recognized python-support module."%arg)
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.")
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):
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))
450 if pyver in py_installed and not os.path.isfile(os.path.join(dir,".path")):
451 need_postinstall.append(pyver)
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)
461 if options.rebuild_all:
462 for package in private_packages:
463 private_packages[package].bytecompile()
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()
474 for package in do_public:
475 need_postinstall = concat (need_postinstall, isect(package.pyversions,py_installed))
476 if options.clean_mode:
478 package.install(py_installed)
480 if f.endswith(".pth"):
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')
488 sys.stderr.write("ERROR: dpkg-trigger failed\n")
491 for py in need_postinstall:
493 need_postinstall = []
495 if options.post_install:
496 # The trigger has been activated; do it for all installed versions
497 need_postinstall = py_installed
500 for py in need_postinstall:
501 post_change_stuff(py)