aotcompile.py.in 14 KB

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