#!/usr/bin/env python # # faster.py, an abbreviated build script for java-gnome # Must be invoked from the project top level directory as [./]build/faster # # Copyright (c) 2007-2008 Operational Dynamics Consulting Pty Ltd # # The code in this file, and the library it is a part of, are made available # to you by the authors under the terms of the "GNU General Public Licence, # version 2". See the LICENCE file for the terms governing usage and # redistribution. # # # This is a total hack, but a good one :) Make has two critical weakness, which # this script, being programmatic, tries to address: # # 1) variables are only populated once, so the variables containing the lists # of files to be built resulting from output of the code generator are # inaccurate (or in a clean build, empty) # # 2) it doesn't look at actual file contents, only looking to see if the target # is older than the source file(s). This is a real problem for people hacking # on the bindings; every time you save a source file in the generator, it runs, # and even though most (even all) of output files are unchanged [ie, were # refilled with exactly the same content], they are newer, and so make charges # ahead with a full rebuild, costing over 5 minutes of CPU time. # # So this program takes an md5sum of each source file at each step, only # invoking the external program for that target if the files have actually # changed since the last run. # # No, this is not some grand replacement for all the worlds problems. It's a # quick hack. I said so already. And it's entirely custom for building # java-gnome. But it encapsulates some of the capabilities that buildtool will # bring to the process when it lands, so it is a step in the right direction. # import os, md5, subprocess, cPickle, sys from os.path import * from shutil import move config = {} hashes = {} verbose = False silent = False configFile = ".config" versionFile = "src/bindings/org/freedesktop/bindings/Version.java" hashFile = "tmp/.hashes" lockFile = "tmp/.build" GNOME_MODULES = "gthread-2.0 glib-2.0 gtk+-2.0 gtk+-unix-print-2.0 libglade-2.0" # # Armour against multiple simultaneous invocations. # # Rumour has it that locking is hard, unsafe, and generally evil. That's all # true. Usage here should be safe enough; this lock is just to prevent # double-taps by Eclipse being overzealous. Question: is this NFS safe? # from fcntl import flock, LOCK_EX, LOCK_NB def lockBuild(): global lock ensureDirectory("tmp/") lock = open(lockFile, "wb") try: flock(lock, LOCK_EX | LOCK_NB) except IOError: if not silent: print "Inhibited: another build process already running" sys.stdout.flush() sys.exit(0) def unlockBuild(): global lock lock.close() # # Read the configuration data from .config # # The ./configure script produces a make fragment full of variables suitable to # be included in our top level Makefile. So long as that Makefile exists we'll # leave it alone, meaning we need to enclose the variable declarations with " # characters before sourcing it into this Python program. # # We could be smart and verify that the requisite data is there, but I imagine # a KeyError will be raised later on if it isn't. # def loadConfig(): global config if ((not isfile(configFile)) or (getmtime(configFile) < getmtime(versionFile))): print print "You need to run ./configure to check prerequisites" print "and setup preferences before you can build java-gnome." if not os.access("configure", os.X_OK): print "I'll make it executable for you." print executeCommand("CHMOD", "configure", "chmod +x configure") else: print sys.exit(1) try: cfg = open(configFile, "r") for line in cfg: if line.find("=") != -1: escaped = line.replace("=", "=\"", 1) escaped = escaped.strip() escaped += "\"" exec(escaped, config) cfg.close() except (EOFError): print "Error while trying to read .config" sys.exit(9) config['GNOME_CCFLAGS'] = os.popen("pkg-config --cflags " + GNOME_MODULES).read().rstrip() config['GNOME_LDFLAGS'] = os.popen("pkg-config --libs " + GNOME_MODULES).read().rstrip() def loadHashes(): global hashes if isfile(hashFile): try: db = open(hashFile, "rb") hashes = cPickle.load(db) db.close() except (EOFError, KeyError, IndexError): print "build checksum cache corrupt; full rebuild forced" hashes = {} # # TODO writing the whole pickle each time must be tremendously inefficient, but # so long as the build is nice and fast, we can leave it be. If someone wants # to try replacing this with bdb or dbm, please give it a try. # def checkpointHashes(): db = open(hashFile + ".tmp", "wb") cPickle.dump(hashes, db) db.close() move(hashFile + ".tmp", hashFile) def ensureDirectory(dir): if isdir(dir): return executeCommand("MKDIR", dir, "mkdir -p " + dir) def touchFile(file): f = open(file, "w") f.close() def prepareBindingsDirectories(): ensureDirectory("tmp/stamp/") ensureDirectory("generated/bindings/") ensureDirectory("tmp/bindings/") ensureDirectory("tmp/generator/") ensureDirectory("tmp/objects/") ensureDirectory("tmp/include/") ensureDirectory("tmp/tests/") def prepareTestDirectories(): ensureDirectory("tmp/tests/") def findFiles(baseDir, ext): result = [] for (root, dirs, files) in os.walk(baseDir): for file in files: if file.endswith(ext): result.append(join(root, file)) return result # # Scan a list of files and decide if they need [re]-building. # # Two things to check: # 1) target older? # 2) if so, has source changed? # # Otherwise, (no target), just # *) has source changed? # # We compare source files' md5sums against the values we have stored in our # hash dictionary. The dictionary is immediately updated but this only has any # persistent effect if a checkpoint happens after a command is run # successfully. FIXME verify! # # Takes a list of touples mapping candidate source files to target filenames # def sourceChanged(file, hash): if hashes.has_key(file): if hashes[file] == hash: return False return True def updateHash(file, hash): hashes[file] = hash def debug(args): if False: print args, def filesNeedBuilding(list, update=True): changed = [] for (source, target) in list: if fileNeedsBuilding(source, target, update): changed.append(source) return changed def fileNeedsBuilding(source, target, update=True): if not isfile(source): sys.exit(source + " missing, abort") f = open(source) m = md5.new(f.read()) f.close() hash = m.hexdigest() debug("CHECK\t"+str(target)+" from "+source+"\n") debug("TARGET",) if not isfile(target): debug("MISSING",) elif getmtime(target) < getmtime(source): debug("OLDER,") if not sourceChanged(source, hash): debug("SOURCE UNCHANGED\n") return False else: debug("NEWER, SKIP\n") return False debug("BUILD\n") if update: updateHash(source, hash) return True # # common use case that source files transform predictably 1:1 into target # files. Return a list of (source, target) touples # def dependsMapSourceFilesToTargetFiles(sourceDir, sourceExt, targetDir, targetExt): list = findFiles(sourceDir, sourceExt) result = [] for source in list: target = source.replace(sourceDir, targetDir) target = target.replace(sourceExt, targetExt) pair = (source, target) result.append(pair) return result # # single target depends on many files. Use this with a stamp if all you really # want to do is check to see if a series of sources have changed # def dependsListToSingleTarget(list, target): result = [] for source in list: pair = (source, target) result.append(pair) return result # # the rather kludgy mapping between .po files and .mo files # def dependsMapTranslationFileToCatalogueFile(domain, poDir, targetDir): list = findFiles(poDir, ".po") result = [] for source in list: target = source.replace(".po", "") target = target.replace(poDir, targetDir) target = target + "/LC_MESSAGES/" + domain + ".mo" pair = (source, target) result.append(pair) return result # # FIXME One fairly glaring weakness of this script is that it doesn't do Nth # order build concurrency in the sense of make -jN. I imagine that given the # sort of semantics that wait() provides we could probably fork off multiple # children. Feel welcome to fix this. # def executeCommand(short, what, cmd, inDir=None): sys.stderr.flush() if not silent: print short + "\t" + what if verbose: print cmd sys.stdout.flush() status = subprocess.call(cmd, shell=True, cwd=inDir, bufsize=1) if status != 0: sys.exit(1) checkpointHashes() sys.stderr.flush() sys.stdout.flush() def compileJavaCode(outputDir, classpath, sourcepath, sources): cmd = config['JAVAC'] + " " cmd += "-d " + outputDir if classpath: cmd += " -classpath " + classpath if sourcepath: cmd += " -sourcepath " + sourcepath cmd += " " + " ".join(sources) blurb = "\n\t".join(sources) executeCommand(config['JAVAC_CMD'], blurb, cmd) def runJavaClass(classname, classpath, args=""): cmd = config['JAVA'] + " " cmd += "-classpath " + classpath + " " cmd += classname if args: cmd += " " + args executeCommand(config['JAVA_CMD'], classname, cmd) def compileGeneratorClasses(): pairs = dependsMapSourceFilesToTargetFiles("src/generator/", ".java", "tmp/generator/", ".class") changed = filesNeedBuilding(pairs) if not changed: return compileJavaCode("tmp/generator/", "", "src/generator/", changed) def generateTranslationAndJniLayers(): list = findFiles("tmp/generator", ".class") list += findFiles("src/defs", ".defs") stamp = "tmp/stamp/generator" redirect = "" pairs = dependsListToSingleTarget(list, stamp) changed = filesNeedBuilding(pairs) if not changed: return if not verbose: redirect = "> /dev/null" runJavaClass("BindingsGenerator", "tmp/generator/", redirect) touchFile(stamp) def compileBindingsClasses(): pairs = dependsMapSourceFilesToTargetFiles("generated/bindings/", ".java", "tmp/bindings/", ".class") pairs += dependsMapSourceFilesToTargetFiles("src/bindings/", ".java", "tmp/bindings/", ".class") changed = filesNeedBuilding(pairs) if not changed: return compileJavaCode("tmp/bindings/", "tmp/bindings/", "src/bindings/:generated/bindings/", changed) # # This seems like a lot of effort to copy a file # def copyMappingFile(): source = "generated/bindings/typeMapping.properties" target = "tmp/bindings/typeMapping.properties" if not fileNeedsBuilding(source, target): return cmd = "cp " + source + " " + target executeCommand("CP", target, cmd) def makeJarFile(): jar = "tmp/gtk-4.0.jar" list = findFiles("tmp/bindings/", ".class") list += findFiles("tmp/bindings/", ".properties") pairs = dependsListToSingleTarget(list, jar) changed = filesNeedBuilding(pairs, False) if not changed: return files = [] for file in list: file = file.replace("tmp/bindings/", "") file = file.replace("$", "\$") files.append(file) cmd = config['JAR'] + " cf " cmd += "../../" + jar + " " cmd += " ".join(files) executeCommand(config['JAR_CMD'], jar, cmd, "tmp/bindings/") def generateHeaderFiles(): list = findFiles("tmp/bindings/", ".class") map_c = {} map_h = {} pairs = [] classes = [] headers = [] for file in list: if file.find("$") != -1: continue t = file.replace("tmp/bindings/", "") t = t.replace(".class", "") c = t.replace("/", ".") t = t.replace("/", "_") t = t.replace("$", "_") t = t + ".h" t = "tmp/include/" + t pairs.append((file, t)) map_c[file] = c map_h[file] = t changed = filesNeedBuilding(pairs) if not changed: return for file in changed: classes.append(map_c[file]) headers.append(map_h[file]) cmd = config['JAVAH'] + " " if verbose: cmd += "-verbose " cmd += "-d tmp/include/ " cmd += "-classpath tmp/bindings/ " cmd += " ".join(classes) if not verbose: cmd += " >/dev/null" blurb = "\n\t".join(headers) executeCommand(config['JAVAH_CMD'], blurb, cmd) def compileCSourceToObject(source, target): ensureDirectory(dirname(target)) if config.has_key('CCACHE'): cmd = config['CCACHE'] + " " else: cmd = "" cmd += config['CC'] + " " cmd += "-Isrc/jni -Itmp/include " cmd += config['GNOME_CCFLAGS'] + " " cmd += "-o " + target + " -c " + source executeCommand(config['CC_CMD'], source, cmd) def compileBindingsObjects(): pairs = dependsMapSourceFilesToTargetFiles("src/bindings/", ".c", "tmp/objects/", ".o") pairs += dependsMapSourceFilesToTargetFiles("generated/bindings/", ".c", "tmp/objects/", ".o") pairs += dependsMapSourceFilesToTargetFiles("src/jni/", ".c", "tmp/objects/", ".o") for (source, target) in pairs: if fileNeedsBuilding(source, target): compileCSourceToObject(source, target) def linkSharedLibrary(): so = "tmp/libgtkjni-" + config['VERSION'] + ".so" list = findFiles("tmp/objects/", ".o") pairs = dependsListToSingleTarget(list, so) changed = filesNeedBuilding(pairs) if not changed: return cmd = config['LINK'] + " " cmd += "-shared " cmd += config['GNOME_LDFLAGS'] + " " cmd += "-o " + so + " " cmd += " ".join(list) executeCommand(config['LINK_CMD'], so, cmd) def compileTestClasses(): pairs = dependsMapSourceFilesToTargetFiles("tests/generator/", ".java", "tmp/tests/", ".class") pairs += dependsMapSourceFilesToTargetFiles("tests/bindings/", ".java", "tmp/tests/", ".class") pairs += dependsMapSourceFilesToTargetFiles("tests/prototype/", ".java", "tmp/tests/", ".class") pairs += dependsMapSourceFilesToTargetFiles("tests/screenshots/", ".java", "tmp/tests/", ".class") pairs += dependsMapSourceFilesToTargetFiles("doc/examples/", ".java", "tmp/tests/", ".class") changed = filesNeedBuilding(pairs) if not changed: return compileJavaCode("tmp/tests/", "tmp/generator:tmp/bindings/:"+config['JUNIT_JARS'], "tests/generator/:tests/bindings/:tests/prototype/:tests/screenshots/:doc/examples/", changed) def compileMessageCatalogue(source, target): ensureDirectory(dirname(target)) cmd = "msgfmt " + "-o " + target + " " + source executeCommand("MSGFMT", target, cmd) # # FIXME this could be better generalized, but in any event this verges on # being unnecessary # def extractInternationalizationTemplates(): list = findFiles("doc/examples/i18n", ".java") template = "tmp/i18n/example.pot" pairs = dependsListToSingleTarget(list, template) changed = filesNeedBuilding(pairs) if not changed: return ensureDirectory(dirname(template)) cmd = "xgettext " cmd += "-o " + template + " " cmd += "--omit-header --keyword=_ --keyword=N_ " cmd += " ".join(list) executeCommand("EXTRACT", template, cmd) def compileTranslationCatalogues(): pairs = dependsMapTranslationFileToCatalogueFile("example", "doc/po/", "tmp/locale/") pairs += dependsMapTranslationFileToCatalogueFile("unittest", "tests/po/", "tmp/locale/") for (source, target) in pairs: if fileNeedsBuilding(source, target): compileMessageCatalogue(source, target) # # main build sequence, with elaborately named methods Carl Rosenberger style # def generateBindings(): prepareBindingsDirectories() compileGeneratorClasses() generateTranslationAndJniLayers() compileBindingsClasses() copyMappingFile() makeJarFile() generateHeaderFiles() compileBindingsObjects() linkSharedLibrary() def generateTests(): prepareTestDirectories() compileTestClasses() extractInternationalizationTemplates() compileTranslationCatalogues() # # Output the API documentation. Owing to the need to configure the standard # doclet on the command line, building up this expression is rather ulgy, and # not the place we'd like to see presentation stuff, all things considered. # # We also run the harness which takes screenshots of the Snapshot classes # which are used to illustrate our docs. # # The logic to have this rendered off screen is buggy at the moment, so you # need to let it run by itself and not take focus away. The code to execute it # is also somewhat fragile and still has a number of hard coded paths. As the # only person who actually has to run this is the maintainer uploading to the # website, this isn't a problem right now, but at some point we'll want to # consider having configure probe for the things that (disabled) code path # depends on. # def compileDocumentation(): cmd = config['JAVADOC'] + " " if not verbose: cmd += "-quiet " cmd += "-d doc/api " cmd += "-classpath tmp/bindings " cmd += "-public " cmd += "-nodeprecated " cmd += "-source 1.5 " cmd += "-notree " cmd += "-noindex " cmd += "-nohelp " cmd += "-version " cmd += "-author " cmd += "-windowtitle 'java-gnome %s API Documentation' " % config['APIVERSION'] cmd += "-doctitle '

java-gnome %s API Documentation

' " % config['APIVERSION'] cmd += "-header 'java-gnome version %s' " % config['VERSION'] cmd += "-footer '
java-gnome' " cmd += "-breakiterator " cmd += "-stylesheetfile src/bindings/stylesheet.css " cmd += "-overview src/bindings/overview.html " cmd += "-sourcepath src/bindings " cmd += "-subpackages org " cmd += "-exclude org.freedesktop.bindings " cmd += "src/bindings/org/freedesktop/bindings/Time.java " cmd += "src/bindings/org/freedesktop/bindings/Version.java " if not verbose: cmd += " >/dev/null" executeCommand(config["JAVADOC_CMD"], "doc/api/*.html", cmd % config) def takeSnapshots(): runJavaClass("Harness", "tmp/gtk-4.0.jar:tmp/tests/") def generateDocumentation(): compileDocumentation() takeSnapshots() # # Final miscallaneous execution targets, taking advantage of the fact that # we've got all this infrastructure to run Java code. # def runTests(): runJavaClass("UnitTests", "tmp/gtk-4.0.jar:tmp/generator/:tmp/tests/:"+config['JUNIT_JARS']) def runDemo(): runJavaClass("button.ExamplePressMe", "tmp/gtk-4.0.jar:tmp/tests/") # # Preliminary setup & main entry point. # from sys import argv def main(): lockBuild() loadConfig() loadHashes() generateBindings() if len(argv) > 1: generateTests() unlockBuild() if len(argv) == 1: return if sys.argv[1] == "doc": generateDocumentation() elif sys.argv[1] == "test": runTests() elif sys.argv[1] == "demo": runDemo() if __name__ == '__main__': if os.getenv("V"): verbose = True if len(argv) > 1 and sys.argv[1] == "ide": silent = True try: main() except KeyboardInterrupt: print