OSDN Git Service

2008-06-30 Joshua Sumali <jsumali@redhat.com>
[pf3gnuchains/gcc-fork.git] / libjava / contrib / aotcompile.py.in
1 # -*- python -*-
2
3 ## Copyright (C) 2005, 2006, 2008 Free Software Foundation
4 ## Written by Gary Benson <gbenson@redhat.com>
5 ##
6 ## This program is free software; you can redistribute it and/or modify
7 ## it under the terms of the GNU General Public License as published by
8 ## the Free Software Foundation; either version 2 of the License, or
9 ## (at your option) any later version.
10 ##
11 ## This program is distributed in the hope that it will be useful,
12 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 ## GNU General Public License for more details.
15
16 import classfile
17 import copy
18 import md5
19 import operator
20 import os
21 import sys
22 import cStringIO as StringIO
23 import zipfile
24
25 PATHS = {"make":   "@MAKE@",
26          "gcj":    "@prefix@/bin/gcj@gcc_suffix@",
27          "dbtool": "@prefix@/bin/gcj-dbtool@gcc_suffix@"}
28
29 MAKEFLAGS = []
30 GCJFLAGS = ["-fPIC", "-findirect-dispatch", "-fjni"]
31 LDFLAGS = ["-Wl,-Bsymbolic"]
32
33 MAX_CLASSES_PER_JAR = 1024
34 MAX_BYTES_PER_JAR = 1048576
35
36 MAKEFILE = "Makefile"
37
38 MAKEFILE_HEADER = '''\
39 GCJ = %(gcj)s
40 DBTOOL = %(dbtool)s
41 GCJFLAGS = %(gcjflags)s
42 LDFLAGS = %(ldflags)s
43
44 %%.o: %%.jar
45         $(GCJ) -c $(GCJFLAGS) $< -o $@
46
47 TARGETS = \\
48 %(targets)s
49
50 all: $(TARGETS)'''
51
52 MAKEFILE_JOB = '''
53 %(base)s_SOURCES = \\
54 %(jars)s
55
56 %(base)s_OBJECTS = \\
57 $(%(base)s_SOURCES:.jar=.o)
58
59 %(dso)s: $(%(base)s_OBJECTS)
60         $(GCJ) -shared $(GCJFLAGS) $(LDFLAGS) $^ -o $@
61
62 %(db)s: $(%(base)s_SOURCES)
63         $(DBTOOL) -n $@ 64
64         for jar in $^; do \\
65             $(DBTOOL) -f $@ $$jar \\
66                 %(libdir)s/%(dso)s; \\
67         done'''
68
69 ZIPMAGIC, CLASSMAGIC = "PK\x03\x04", "\xca\xfe\xba\xbe"
70
71 class Error(Exception):
72     pass
73
74 class Compiler:
75     def __init__(self, srcdir, libdir, prefix = None):
76         self.srcdir = os.path.abspath(srcdir)
77         self.libdir = os.path.abspath(libdir)
78         if prefix is None:
79             self.dstdir = self.libdir
80         else:
81             self.dstdir = os.path.join(prefix, self.libdir.lstrip(os.sep))
82
83         # Calling code may modify these parameters
84         self.gcjflags = copy.copy(GCJFLAGS)
85         self.ldflags = copy.copy(LDFLAGS)
86         self.makeflags = copy.copy(MAKEFLAGS)
87         self.exclusions = []
88
89     def compile(self):
90         """Search srcdir for classes and jarfiles, then generate
91         solibs and mappings databases for them all in libdir."""
92         if not os.path.isdir(self.dstdir):
93             os.makedirs(self.dstdir)
94         oldcwd = os.getcwd()
95         os.chdir(self.dstdir)
96         try:            
97             jobs = self.getJobList()
98             if not jobs:
99                 raise Error, "nothing to do"
100             self.writeMakefile(MAKEFILE, jobs)
101             for job in jobs:
102                 job.writeJars()
103             system([PATHS["make"]] + self.makeflags)
104             for job in jobs:
105                 job.clean()
106             os.unlink(MAKEFILE)
107         finally:
108             os.chdir(oldcwd)
109
110     def getJobList(self):
111         """Return all jarfiles and class collections in srcdir."""
112         jobs = weed_jobs(find_jobs(self.srcdir, self.exclusions))
113         set_basenames(jobs)
114         return jobs
115
116     def writeMakefile(self, path, jobs):
117         """Generate a makefile to build the solibs and mappings
118         databases for the specified list of jobs."""
119         fp = open(path, "w")
120         print >>fp, MAKEFILE_HEADER % {
121             "gcj": PATHS["gcj"],
122             "dbtool": PATHS["dbtool"],
123             "gcjflags": " ".join(self.gcjflags),
124             "ldflags": " ".join(self.ldflags),
125             "targets": " \\\n".join(reduce(operator.add, [
126                 (job.dsoName(), job.dbName()) for job in jobs]))}
127         for job in jobs:
128             values = job.ruleArguments()
129             values["libdir"] = self.libdir
130             print >>fp, MAKEFILE_JOB % values
131         fp.close()
132
133 def find_jobs(dir, exclusions = ()):
134     """Scan a directory and find things to compile: jarfiles (zips,
135     wars, ears, rars, etc: we go by magic rather than file extension)
136     and directories of classes."""
137     def visit((classes, zips), dir, items):
138         for item in items:
139             path = os.path.join(dir, item)
140             if os.path.islink(path) or not os.path.isfile(path):
141                 continue
142             magic = open(path, "r").read(4)
143             if magic == ZIPMAGIC:
144                 zips.append(path)
145             elif magic == CLASSMAGIC:
146                 classes.append(path)
147     classes, paths = [], []
148     os.path.walk(dir, visit, (classes, paths))
149     # Convert the list of classes into a list of directories
150     while classes:
151         # XXX this requires the class to be correctly located in its heirachy.
152         path = classes[0][:-len(os.sep + classname(classes[0]) + ".class")]
153         paths.append(path)
154         classes = [cls for cls in classes if not cls.startswith(path)]
155     # Handle exclusions.  We're really strict about them because the
156     # option is temporary in aot-compile-rpm and dead options left in
157     # specfiles will hinder its removal.
158     for path in exclusions:
159         if path in paths:
160             paths.remove(path)
161         else:
162             raise Error, "%s: path does not exist or is not a job" % path
163     # Build the list of jobs
164     jobs = []
165     paths.sort()
166     for path in paths:
167         if os.path.isfile(path):
168             job = JarJob(path)
169         else:
170             job = DirJob(path)
171         if len(job.classes):
172             jobs.append(job)
173     return jobs
174
175 class Job:
176     """A collection of classes that will be compiled as a unit."""
177     
178     def __init__(self, path):
179         self.path, self.classes, self.blocks = path, {}, None
180
181     def addClass(self, bytes):
182         """Subclasses call this from their __init__ method for
183         every class they find."""
184         self.classes[md5.new(bytes).digest()] = bytes
185
186     def __makeBlocks(self):
187         """Split self.classes into chunks that can be compiled to
188         native code by gcj.  In the majority of cases this is not
189         necessary -- the job will have come from a jarfile which will
190         be equivalent to the one we generate -- but this only happens
191         _if_ the job was a jarfile and _if_ the jarfile isn't too big
192         and _if_ the jarfile has the correct extension and _if_ all
193         classes are correctly named and _if_ the jarfile has no
194         embedded jarfiles.  Fitting a special case around all these
195         conditions is tricky to say the least.
196
197         Note that this could be called at the end of each subclass's
198         __init__ method.  The reason this is not done is because we
199         need to parse every class file.  This is slow, and unnecessary
200         if the job is subsetted."""
201         names = {}
202         for hash, bytes in self.classes.items():
203             name = classname(bytes)
204             if not names.has_key(name):
205                 names[name] = []
206             names[name].append(hash)
207         names = names.items()
208         # We have to sort somehow, or the jars we generate 
209         # We sort by name in a simplistic attempt to keep related
210         # classes together so inter-class optimisation can happen.
211         names.sort()
212         self.blocks, bytes = [[]], 0
213         for name, hashes in names:
214             for hash in hashes:
215                 if len(self.blocks[-1]) >= MAX_CLASSES_PER_JAR \
216                    or bytes >= MAX_BYTES_PER_JAR:
217                     self.blocks.append([])
218                     bytes = 0
219                 self.blocks[-1].append((name, hash))
220                 bytes += len(self.classes[hash])
221
222     # From Archit Shah:
223     #   The implementation and the documentation don't seem to match.
224     #  
225     #    [a, b].isSubsetOf([a]) => True
226     #  
227     #   Identical copies of all classes this collection do not exist
228     #   in the other. I think the method should be named isSupersetOf
229     #   and the documentation should swap uses of "this" and "other"
230     #
231     # XXX think about this when I've had more sleep...
232     def isSubsetOf(self, other):
233         """Returns True if identical copies of all classes in this
234         collection exist in the other."""
235         for item in other.classes.keys():
236             if not self.classes.has_key(item):
237                 return False
238         return True
239
240     def __targetName(self, ext):
241         return self.basename + ext
242
243     def tempJarName(self, num):
244         return self.__targetName(".%d.jar" % (num + 1))
245
246     def tempObjName(self, num):
247         return self.__targetName(".%d.o" % (num + 1))
248
249     def dsoName(self):
250         """Return the filename of the shared library that will be
251         built from this job."""
252         return self.__targetName(".so")
253
254     def dbName(self):
255         """Return the filename of the mapping database that will be
256         built from this job."""
257         return self.__targetName(".db")
258
259     def ruleArguments(self):
260         """Return a dictionary of values that when substituted
261         into MAKEFILE_JOB will create the rules required to build
262         the shared library and mapping database for this job."""
263         if self.blocks is None:
264             self.__makeBlocks()
265         return {
266             "base": "".join(
267                 [c.isalnum() and c or "_" for c in self.dsoName()]),
268             "jars": " \\\n".join(
269                 [self.tempJarName(i) for i in xrange(len(self.blocks))]),
270             "dso": self.dsoName(),
271             "db": self.dbName()}
272
273     def writeJars(self):
274         """Generate jarfiles that can be native compiled by gcj."""
275         if self.blocks is None:
276             self.__makeBlocks()
277         for block, i in zip(self.blocks, xrange(len(self.blocks))):
278             jar = zipfile.ZipFile(self.tempJarName(i), "w", zipfile.ZIP_STORED)
279             for name, hash in block:
280                 jar.writestr(
281                     zipfile.ZipInfo("%s.class" % name), self.classes[hash])
282             jar.close()
283
284     def clean(self):
285         """Delete all temporary files created during this job's build."""
286         if self.blocks is None:
287             self.__makeBlocks()
288         for i in xrange(len(self.blocks)):
289             os.unlink(self.tempJarName(i))
290             os.unlink(self.tempObjName(i))
291
292 class JarJob(Job):
293     """A Job whose origin was a jarfile."""
294
295     def __init__(self, path):
296         Job.__init__(self, path)
297         self._walk(zipfile.ZipFile(path, "r"))
298
299     def _walk(self, zf):
300         for name in zf.namelist():
301             bytes = zf.read(name)
302             if bytes.startswith(ZIPMAGIC):
303                 self._walk(zipfile.ZipFile(StringIO.StringIO(bytes)))
304             elif bytes.startswith(CLASSMAGIC):
305                 self.addClass(bytes)
306
307 class DirJob(Job):
308     """A Job whose origin was a directory of classfiles."""
309
310     def __init__(self, path):
311         Job.__init__(self, path)
312         os.path.walk(path, DirJob._visit, self)
313
314     def _visit(self, dir, items):
315         for item in items:
316             path = os.path.join(dir, item)
317             if os.path.islink(path) or not os.path.isfile(path):
318                 continue
319             fp = open(path, "r")
320             magic = fp.read(4)
321             if magic == CLASSMAGIC:
322                 self.addClass(magic + fp.read())
323     
324 def weed_jobs(jobs):
325     """Remove any jarfiles that are completely contained within
326     another.  This is more common than you'd think, and we only
327     need one nativified copy of each class after all."""
328     jobs = copy.copy(jobs)
329     while True:
330         for job1 in jobs:
331             for job2 in jobs:
332                 if job1 is job2:
333                     continue
334                 if job1.isSubsetOf(job2):
335                     msg = "subsetted %s" % job2.path
336                     if job2.isSubsetOf(job1):
337                         if (isinstance(job1, DirJob) and
338                             isinstance(job2, JarJob)):
339                             # In the braindead case where a package
340                             # contains an expanded copy of a jarfile
341                             # the jarfile takes precedence.
342                             continue
343                         msg += " (identical)"
344                     warn(msg)
345                     jobs.remove(job2)
346                     break
347             else:
348                 continue
349             break
350         else:
351             break
352         continue
353     return jobs
354
355 def set_basenames(jobs):
356     """Ensure that each jarfile has a different basename."""
357     names = {}
358     for job in jobs:
359         name = os.path.basename(job.path)
360         if not names.has_key(name):
361             names[name] = []
362         names[name].append(job)
363     for name, set in names.items():
364         if len(set) == 1:
365             set[0].basename = name
366             continue
367         # prefix the jar filenames to make them unique
368         # XXX will not work in most cases -- needs generalising
369         set = [(job.path.split(os.sep), job) for job in set]
370         minlen = min([len(bits) for bits, job in set])
371         set = [(bits[-minlen:], job) for bits, job in set]
372         bits = apply(zip, [bits for bits, job in set])
373         while True:
374             row = bits[-2]
375             for bit in row[1:]:
376                 if bit != row[0]:
377                     break
378             else:
379                 del bits[-2]
380                 continue
381             break
382         set = zip(
383             ["_".join(name) for name in apply(zip, bits[-2:])],
384             [job for bits, job in set])
385         for name, job in set:
386             warn("building %s as %s" % (job.path, name))
387             job.basename = name
388     # XXX keep this check until we're properly general
389     names = {}
390     for job in jobs:
391         name = job.basename
392         if names.has_key(name):
393             raise Error, "%s: duplicate jobname" % name
394         names[name] = 1
395
396 def system(command):
397     """Execute a command."""
398     status = os.spawnv(os.P_WAIT, command[0], command)
399     if status > 0:
400         raise Error, "%s exited with code %d" % (command[0], status)
401     elif status < 0:
402         raise Error, "%s killed by signal %d" % (command[0], -status)
403
404 def warn(msg):
405     """Print a warning message."""
406     print >>sys.stderr, "%s: warning: %s" % (
407         os.path.basename(sys.argv[0]), msg)
408
409 def classname(bytes):
410     """Extract the class name from the bytes of a class file."""
411     klass = classfile.Class(bytes)
412     return klass.constants[klass.constants[klass.name][1]][1]