Nailed it down
Xcode has been driving me mad recently. I am writing TINI applications in Java, which requires me to feed the class files to a converter application. I had a little trouble figuring out how to make Xcode generate and run on a class hierarchy instead of a jar file but I will talk about it in details in an upcoming entry. The topic today is the bug I discovered (and fixed!) in Xcode when working with class hierarchies.
Since I already wrote about it several times for the Java Dev and Xcode Users mailing lists, I will limit myself to posting the report I submitted to Apple.
1. Configuration
- Mac OS X 10.3.5
- Xcode 1.5
- Java 1.4.2 Update 1
- Java 1.4.2 Update 1 Developer Tools
2. Summary
When configuring a Java Tool project to generate a class hierarchy instead of a Java archive, the class files are copied into TARGET_BUILD_DIR/PRODUCT_NAME
(i.e. PRODUCT_CLASS_FILE_DIR
) the first time the project is built. However, on subsequent builds, the classes in that folder are not updated, even when the sources are. Therefore, the project runs on old class files and so do potential shell script build phases that would post process the class files.
3. Steps to reproduce
- Create a new Java Tool project in Xcode;
- In the active target, change the Java Archive Settings / Product type to Class Hierarchy (or in expert mode, set
JAVA_ARCHIVE_CLASSES = NO
); - Build the project;
- In the Finder, take note the modification date of the class file in
build/PRODUCT_NAME
; - Make a modification to the sources, e.g. change
"Hello World!"
to"Take over the World!"
; - Save and build the project;
- In the Finder, notice that the class file has not been updated since the modification date did not change.
4. Expected results
Every time the project is built, the class hierarchy should be copied from CLASS_FILE_DIR
to PRODUCT_CLASS_FILE_DIR
if it has been modified, as it happens the first time the project is built.
5. Actual results
The class hierarchy is only copied on the first build or after a target clean-up.
6. Cause
Watching the build process from a shell using xcodebuild
instead of the Xcode GUI shows that on a clean target, after the Java Archive Files build phase, the PRODUCT_CLASS_FILE_DIR
is created and ditto
is used to copy the class files from CLASS_FILE_DIR
to PRODUCT_CLASS_FILE_DIR
:
BuildPhase <JavaArchiveFiles>Hello
echo Completed phase "<JavaArchiveFiles>" for "<JavaArchiveFiles>Hello"
Completed phase <JavaArchiveFiles> for <JavaArchiveFiles>Hello
Mkdir […]/Hello/build/Hello
/bin/mkdir -p […]/Hello/build/Hello
Ditto […]/Hello/build/Hello
/usr/bin/ditto […]/Hello/build/Hello.build/Hello.build\
/JavaClasses […]/Hello/build/Hello
However, when PRODUCT_CLASS_FILE_DIR
exists, neither of these two operations is executed.
7. Workaround
Cleaning the active target prior to building or deleting the PRODUCT_CLASS_FILE_DIR
folder causes Xcode to create it again, with up-to-date files. However, emptying this folder is not enough.
8. Resolution
The build instructions are defined in
/Developer/Makefiles/pbx_jamfiles/ProjectBuilderJambase.
The parts of interest are the definition of Mkdir
(ProjectBuilderJambase
, line 299):
# Mkdir <directory>
# Creates <directory>
rule Mkdir
{
# Only existence of the directory matters
NOUPDATE $(1) ;
}
actions together piecemeal Mkdir
{
$(MKDIR) -p $(1:Q)
}
and the section where the class files are copied using ditto
, where it looks like someone has been bothered by this bug for some time (line 4267):
if $(JAVA_ARCHIVE_CLASSES) != YES {
# !!!:cmolick:20020123 product class file dir not always made?!
Mkdir $(PRODUCT_CLASS_FILE_DIR) ;
ProductFile $(PRODUCT_CLASS_FILE_DIR) ;
Ditto $(PRODUCT_CLASS_FILE_DIR) : $(CLASS_FILE_DIR) ;
if $(MERGED_ARCHIVES) {
DEPENDS $(PRODUCT_CLASS_FILE_DIR) : $(MERGED_ARCHIVES) ;
}
else {
DEPENDS $(PRODUCT_CLASS_FILE_DIR) : $(JAVA_COMPILE_TARGET) ;
}
}
It is actually the Mkdir
command that prevents the copy to execute, even though mkdir -p
exits 0 regardless of whether the directory to create is already present. I have no idea whatsoever of the language used to write ProjectBuilderJambase
but I can make a pretty safe guess that the culprit is the NOUPDATE
statement in the Mkdir
rule. Indeed, removing it solves the problem, albeit not in a very clean way as this statement is probably necessary elsewhere (although it might be problematic elsewhere as well).
The fix I implemented was to create a new action, ForceMkDir
, to use in this particular case (ProjectBuilderJambase
, line 310):
# ForceMkdir <directory>
# Creates <directory>, even if it already exists.
#
# This modification is for the Ditto step that copies class files in
# Java projects (line 4267-78). The original Mkdir action above seems
# to prevent ditto from executing when the directory already exists.
#
# Corrected 2004-08-12 by Ölbaum, who's quite proud of having fixed a
# glitch in a program written in a language he does not even know the
# name of.
actions together piecemeal ForceMkdir
{
$(MKDIR) -p $(1:Q)
}
and (line 4269):
# !!!!:Ölbaum:20040812 fixed!
ForceMkdir $(PRODUCT_CLASS_FILE_DIR) ;
9. Enclosures
A patch file for these modifications: ProjectBuilderJambase.diff.
10. Conclusion
After applying these modifications, the class hierarchy in PRODUCT_CLASS_FILE_DIR
is properly updated each time the classes are recompiled.