2
# vim: set fileencoding=UTF-8
4
# java-gnome, a UI library for writing GTK and GNOME programs from Java!
6
# Copyright © 2007-2011 Operational Dynamics Consulting, Pty Ltd
8
# The code in this file, and the program it is a part of, is made available
9
# to you by its authors as open source software: you can redistribute it
10
# and/or modify it under the terms of the GNU General Public License version
11
# 2 ("GPL") as published by the Free Software Foundation.
13
# This program is distributed in the hope that it will be useful, but WITHOUT
14
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15
# FITNESS FOR A PARTICULAR PURPOSE. See the GPL for more details.
17
# You should have received a copy of the GPL along with this program. If not,
18
# see http://www.gnu.org/licenses/. The authors of this program may be
19
# contacted through http://java-gnome.sourceforge.net/.
23
# faster.py, an abbreviated build script for java-gnome
24
# Must be invoked from the project top level directory as [./]build/faster
28
# This is a total hack, but a good one :) Make has two critical weakness, which
29
# this script, being programmatic, tries to address:
31
# 1) variables are only populated once, so the variables containing the lists
32
# of files to be built resulting from output of the code generator are
33
# inaccurate (or in a clean build, empty)
35
# 2) it doesn't look at actual file contents, only looking to see if the target
36
# is older than the source file(s). This is a real problem for people hacking
37
# on the bindings; every time you save a source file in the generator, it runs,
38
# and even though most (even all) of output files are unchanged [ie, were
39
# refilled with exactly the same content], they are newer, and so make charges
40
# ahead with a full rebuild, costing over 5 minutes of CPU time.
42
# So this program takes an md5sum of each source file at each step, only
43
# invoking the external program for that target if the files have actually
44
# changed since the last run.
46
# No, this is not some grand replacement for all the worlds problems. It's a
47
# quick hack. I said so already. And it's entirely custom for building
48
# java-gnome. But it encapsulates some of the capabilities that buildtool will
49
# bring to the process when it lands, so it is a step in the right direction.
52
import os, hashlib, subprocess, cPickle, sys
54
from shutil import move
61
configFile = ".config"
62
versionFile = "src/bindings/org/freedesktop/bindings/Version.java"
63
hashFile = "tmp/.hashes"
64
lockFile = "tmp/.build"
66
GNOME_MODULES = "gthread-2.0 glib-2.0 gtk+-3.0 gtk+-unix-print-3.0 gtksourceview-3.0 libnotify enchant librsvg-2.0"
69
# Armour against multiple simultaneous invocations.
71
# Rumour has it that locking is hard, unsafe, and generally evil. That's all
72
# true. Usage here should be safe enough; this lock is just to prevent
73
# double-taps by Eclipse being overzealous. Question: is this NFS safe?
76
from fcntl import flock, LOCK_EX, LOCK_NB
81
ensureDirectory("tmp/")
83
lock = open(lockFile, "wb")
85
flock(lock, LOCK_EX | LOCK_NB)
88
print "Inhibited: another build process already running"
98
# Read the configuration data from .config
100
# The ./configure script produces a make fragment full of variables suitable to
101
# be included in our top level Makefile. So long as that Makefile exists we'll
102
# leave it alone, meaning we need to enclose the variable declarations with "
103
# characters before sourcing it into this Python program.
105
# We could be smart and verify that the requisite data is there, but I imagine
106
# a KeyError will be raised later on if it isn't.
111
if ((not isfile(configFile)) or (getmtime(configFile) < getmtime(versionFile))):
113
print "You need to run ./configure to check prerequisites"
114
print "and setup preferences before you can build java-gnome."
116
if not os.access("configure", os.X_OK):
117
print "I'll make it executable for you."
119
executeCommand("CHMOD", "configure", "chmod +x configure")
125
cfg = open(configFile, "r")
127
if line.find("=") != -1:
128
escaped = line.replace("=", "=\"", 1)
129
escaped = escaped.strip()
131
exec(escaped, config)
135
print "Error while trying to read .config"
138
config['GNOME_CCFLAGS'] = os.popen("pkg-config --cflags " + GNOME_MODULES).read().rstrip()
139
config['GNOME_LDFLAGS'] = os.popen("pkg-config --libs " + GNOME_MODULES).read().rstrip()
146
db = open(hashFile, "rb")
147
hashes = cPickle.load(db)
149
except (EOFError, KeyError, IndexError):
150
print "build checksum cache corrupt; full rebuild forced"
155
# TODO writing the whole pickle each time must be tremendously inefficient, but
156
# so long as the build is nice and fast, we can leave it be. If someone wants
157
# to try replacing this with bdb or dbm, please give it a try.
160
def checkpointHashes():
161
db = open(hashFile + ".tmp", "wb")
162
cPickle.dump(hashes, db)
165
move(hashFile + ".tmp", hashFile)
168
def ensureDirectory(dir):
171
executeCommand("MKDIR", dir, "mkdir -p " + dir)
179
def prepareBindingsDirectories():
180
ensureDirectory("tmp/stamp/")
181
ensureDirectory("generated/bindings/")
182
ensureDirectory("tmp/bindings/")
183
ensureDirectory("tmp/generator/")
184
ensureDirectory("tmp/objects/")
185
ensureDirectory("tmp/include/")
186
ensureDirectory("tmp/tests/")
188
def prepareTestDirectories():
189
ensureDirectory("tmp/tests/")
191
def findFiles(baseDir, ext):
193
for (root, dirs, files) in os.walk(baseDir):
195
if file.endswith(ext):
196
result.append(join(root, file))
201
# Scan a list of files and decide if they need [re]-building.
203
# Two things to check:
205
# 2) if so, has source changed?
207
# Otherwise, (no target), just
209
# *) has source changed?
211
# We compare source files' md5sums against the values we have stored in our
212
# hash dictionary. The dictionary is immediately updated but this only has any
213
# persistent effect if a checkpoint happens after a command is run
214
# successfully. FIXME verify!
216
# Takes a list of touples mapping candidate source files to target filenames
219
def sourceChanged(file, hash):
220
if hashes.has_key(file):
221
if hashes[file] == hash:
226
def updateHash(file, hash):
235
def filesNeedBuilding(list, update=True):
237
for (source, target) in list:
238
if fileNeedsBuilding(source, target, update):
239
changed.append(source)
243
def fileNeedsBuilding(source, target, update=True):
244
if not isfile(source):
245
sys.exit(source + " missing, abort")
248
m = hashlib.md5(f.read())
252
debug("CHECK\t"+str(target)+" from "+source+"\n")
255
if not isfile(target):
257
elif getmtime(target) < getmtime(source):
259
if not sourceChanged(source, hash):
260
debug("SOURCE UNCHANGED\n")
263
debug("NEWER, SKIP\n")
268
updateHash(source, hash)
273
# common use case that source files transform predictably 1:1 into target
274
# files. Return a list of (source, target) touples
277
def dependsMapSourceFilesToTargetFiles(sourceDir, sourceExt, targetDir, targetExt):
278
list = findFiles(sourceDir, sourceExt)
282
target = source.replace(sourceDir, targetDir)
283
target = target.replace(sourceExt, targetExt)
285
pair = (source, target)
291
# single target depends on many files. Use this with a stamp if all you really
292
# want to do is check to see if a series of sources have changed
295
def dependsListToSingleTarget(list, target):
299
pair = (source, target)
306
# the rather kludgy mapping between .po files and .mo files
309
def dependsMapTranslationFileToCatalogueFile(domain, poDir, targetDir):
310
list = findFiles(poDir, ".po")
314
target = source.replace(".po", "")
315
target = target.replace(poDir, targetDir)
316
target = target + "/LC_MESSAGES/" + domain + ".mo"
318
pair = (source, target)
325
# FIXME One fairly glaring weakness of this script is that it doesn't do Nth
326
# order build concurrency in the sense of make -jN. I imagine that given the
327
# sort of semantics that wait() provides we could probably fork off multiple
328
# children. Feel welcome to fix this.
331
def executeCommand(short, what, cmd, inDir=None):
334
print short + "\t" + what
339
status = subprocess.call(cmd, shell=True, cwd=inDir, bufsize=1)
348
def compileJavaCode(outputDir, classpath, sourcepath, sources):
349
cmd = config['JAVAC'] + " "
350
cmd += "-d " + outputDir
352
cmd += " -classpath " + classpath
354
cmd += " -sourcepath " + sourcepath
355
cmd += " -encoding UTF-8"
356
cmd += " " + " ".join(sources)
358
blurb = "\n\t".join(sources)
360
executeCommand(config['JAVAC_CMD'], blurb, cmd)
363
def runJavaClass(classname, classpath, args=""):
364
cmd = config['JAVA'] + " "
365
cmd += "-classpath " + classpath + " "
371
executeCommand(config['JAVA_CMD'], classname, cmd)
374
def compileGeneratorClasses():
375
pairs = dependsMapSourceFilesToTargetFiles("src/generator/", ".java", "tmp/generator/", ".class")
376
changed = filesNeedBuilding(pairs)
379
compileJavaCode("tmp/generator/", "", "src/generator/", changed)
382
def generateTranslationAndJniLayers():
383
list = findFiles("tmp/generator", ".class")
384
list += findFiles("src/defs", ".defs")
385
stamp = "tmp/stamp/generator"
388
pairs = dependsListToSingleTarget(list, stamp)
390
changed = filesNeedBuilding(pairs)
395
redirect = "> /dev/null"
397
runJavaClass("BindingsGenerator", "tmp/generator/", redirect)
401
def compileBindingsClasses():
402
pairs = dependsMapSourceFilesToTargetFiles("generated/bindings/", ".java", "tmp/bindings/", ".class")
403
pairs += dependsMapSourceFilesToTargetFiles("src/bindings/", ".java", "tmp/bindings/", ".class")
405
changed = filesNeedBuilding(pairs)
409
compileJavaCode("tmp/bindings/", "tmp/bindings/", "src/bindings/:generated/bindings/", changed)
413
# This seems like a lot of effort to copy a file
416
def copyMappingFile():
417
source = "generated/bindings/typeMapping.properties"
418
target = "tmp/bindings/typeMapping.properties"
420
if not fileNeedsBuilding(source, target):
423
cmd = "cp " + source + " " + target
424
executeCommand("CP", target, cmd)
428
jar = "tmp/gtk-4.1.jar"
430
list = findFiles("tmp/bindings/", ".class")
431
list += findFiles("tmp/bindings/", ".properties")
432
pairs = dependsListToSingleTarget(list, jar)
434
changed = filesNeedBuilding(pairs, False)
440
file = file.replace("tmp/bindings/", "")
441
file = file.replace("$", "\$")
444
cmd = config['JAR'] + " cf "
445
cmd += "../../" + jar + " "
446
cmd += " ".join(files)
448
executeCommand(config['JAR_CMD'], jar, cmd, "tmp/bindings/")
451
def generateHeaderFiles():
452
list = findFiles("tmp/bindings/", ".class")
460
if file.find("$") != -1:
462
t = file.replace("tmp/bindings/", "")
463
t = t.replace(".class", "")
464
c = t.replace("/", ".")
466
t = t.replace("/", "_")
467
t = t.replace("$", "_")
469
t = "tmp/include/" + t
471
pairs.append((file, t))
475
changed = filesNeedBuilding(pairs)
480
classes.append(map_c[file])
481
headers.append(map_h[file])
483
cmd = config['JAVAH'] + " "
486
cmd += "-d tmp/include/ "
487
cmd += "-classpath tmp/bindings/ "
488
cmd += " ".join(classes)
492
blurb = "\n\t".join(headers)
494
executeCommand(config['JAVAH_CMD'], blurb, cmd)
497
def compileCSourceToObject(source, target):
498
ensureDirectory(dirname(target))
500
if config.has_key('CCACHE'):
501
cmd = config['CCACHE'] + " "
504
cmd += config['CC'] + " "
505
cmd += "-Isrc/jni -Itmp/include "
506
cmd += config['GNOME_CCFLAGS'] + " "
507
if os.getenv("CFLAGS"):
508
cmd += os.getenv("CFLAGS") + " "
509
cmd += "-o " + target + " -c " + source
511
executeCommand(config['CC_CMD'], source, cmd)
514
def compileBindingsObjects():
515
pairs = dependsMapSourceFilesToTargetFiles("src/bindings/", ".c", "tmp/objects/", ".o")
516
pairs += dependsMapSourceFilesToTargetFiles("generated/bindings/", ".c", "tmp/objects/", ".o")
517
pairs += dependsMapSourceFilesToTargetFiles("src/jni/", ".c", "tmp/objects/", ".o")
519
for (source, target) in pairs:
520
if fileNeedsBuilding(source, target):
521
compileCSourceToObject(source, target)
524
def linkSharedLibrary():
525
so = "tmp/libgtkjni-" + config['VERSION'] + ".so"
527
list = findFiles("tmp/objects/", ".o")
528
pairs = dependsListToSingleTarget(list, so)
530
changed = filesNeedBuilding(pairs)
534
cmd = config['LINK'] + " "
535
if os.getenv("LDFLAGS"):
536
cmd += os.getenv("LDFLAGS") + " "
537
cmd += "-o " + so + " "
538
cmd += " ".join(list) + " "
539
cmd += config['GNOME_LDFLAGS'] + " "
541
executeCommand(config['LINK_CMD'], so, cmd)
544
def compileTestClasses():
545
pairs = dependsMapSourceFilesToTargetFiles("tests/generator/", ".java", "tmp/tests/", ".class")
546
pairs += dependsMapSourceFilesToTargetFiles("tests/bindings/", ".java", "tmp/tests/", ".class")
547
pairs += dependsMapSourceFilesToTargetFiles("tests/prototype/", ".java", "tmp/tests/", ".class")
548
pairs += dependsMapSourceFilesToTargetFiles("tests/screenshots/", ".java", "tmp/tests/", ".class")
549
pairs += dependsMapSourceFilesToTargetFiles("doc/examples/", ".java", "tmp/tests/", ".class")
551
changed = filesNeedBuilding(pairs)
555
compileJavaCode("tmp/tests/", "tmp/generator:tmp/bindings/:"+config['JUNIT_JARS'], "tests/generator/:tests/bindings/:tests/prototype/:tests/screenshots/:doc/examples/", changed)
558
def compileMessageCatalogue(source, target):
559
ensureDirectory(dirname(target))
561
cmd = "msgfmt " + "-o " + target + " " + source
562
executeCommand("MSGFMT", target, cmd)
566
# FIXME this could be better generalized, but in any event this verges on
570
def extractInternationalizationTemplates():
571
list = findFiles("doc/examples/i18n", ".java")
572
template = "tmp/i18n/example.pot"
574
pairs = dependsListToSingleTarget(list, template)
576
changed = filesNeedBuilding(pairs)
580
ensureDirectory(dirname(template))
583
cmd += "--from-code=UTF-8 "
584
cmd += "-o " + template + " "
585
cmd += "--omit-header --keyword=_ --keyword=N_ "
586
cmd += " ".join(list)
588
executeCommand("EXTRACT", template, cmd)
591
def compileTranslationCatalogues():
592
pairs = dependsMapTranslationFileToCatalogueFile("example", "doc/po/", "tmp/locale/")
593
pairs += dependsMapTranslationFileToCatalogueFile("unittest", "tests/po/", "tmp/locale/")
595
for (source, target) in pairs:
596
if fileNeedsBuilding(source, target):
597
compileMessageCatalogue(source, target)
601
# main build sequence, with elaborately named methods Carl Rosenberger style
604
def generateBindings():
605
prepareBindingsDirectories()
607
compileGeneratorClasses()
608
generateTranslationAndJniLayers()
610
compileBindingsClasses()
615
generateHeaderFiles()
616
compileBindingsObjects()
621
prepareTestDirectories()
624
extractInternationalizationTemplates()
625
compileTranslationCatalogues()
629
# Output the API documentation. Owing to the need to configure the standard
630
# doclet on the command line, building up this expression is rather ulgy, and
631
# not the place we'd like to see presentation stuff, all things considered.
633
# We also run the harness which takes screenshots of the Snapshot classes
634
# which are used to illustrate our docs.
636
# The logic to have this rendered off screen is buggy at the moment, so you
637
# need to let it run by itself and not take focus away. The code to execute it
638
# is also somewhat fragile and still has a number of hard coded paths. As the
639
# only person who actually has to run this is the maintainer uploading to the
640
# website, this isn't a problem right now, but at some point we'll want to
641
# consider having configure probe for the things that (disabled) code path
645
def compileDocumentation():
646
cmd = config['JAVADOC'] + " "
652
cmd += "-nodeprecated "
653
cmd += "-source 1.5 "
656
cmd += "-notimestamp "
660
cmd += "-windowtitle 'java-gnome %s API Documentation' " % config['VERSION']
661
cmd += "-doctitle '<h1>java-gnome %s API Documentation</h1>' " % config['VERSION']
662
cmd += "-header 'java-gnome version %s' " % config['VERSION']
663
cmd += "-footer '<img src=\"/images/java-gnome_JavaDocLogo.png\" style=\"padding-right:25px;\"><br> <span style=\"font-family: Arial; font-style: normal; font-size: large;\">java-gnome</span>' "
664
cmd += "-breakiterator "
665
cmd += "-stylesheetfile src/bindings/stylesheet.css "
666
cmd += "-overview src/bindings/overview.html "
667
cmd += "-sourcepath src/bindings:generated/bindings "
668
cmd += "-encoding UTF-8 "
669
cmd += "-subpackages org "
670
cmd += "-exclude org.freedesktop.bindings "
671
cmd += "-noqualifier 'java.*:com.sun.*' "
672
cmd += "src/bindings/org/freedesktop/bindings/Time.java "
673
cmd += "src/bindings/org/freedesktop/bindings/Version.java "
674
cmd += "src/bindings/org/freedesktop/bindings/Internationalization.java "
678
executeCommand(config["JAVADOC_CMD"], "doc/api/*.html", cmd % config)
682
runJavaClass("Harness", "tmp/gtk-4.1.jar:tmp/tests/")
685
def generateDocumentation():
686
compileDocumentation()
690
# Final miscallaneous execution targets, taking advantage of the fact that
691
# we've got all this infrastructure to run Java code.
695
runJavaClass("UnitTests", "tmp/gtk-4.1.jar:tmp/generator/:tmp/tests/:"+config['JUNIT_JARS'])
698
runJavaClass("button.ExamplePressMe", "tmp/gtk-4.1.jar:tmp/tests/")
702
# Preliminary setup & main entry point.
722
if sys.argv[1] == "doc":
723
generateDocumentation()
725
elif sys.argv[1] == "test":
728
elif sys.argv[1] == "demo":
732
if __name__ == '__main__':
736
if len(argv) > 1 and sys.argv[1] == "ide":
741
except KeyboardInterrupt: