/* | |
* | |
* Copyright 2002-2004 The Ant-Contrib project | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package net.sf.antcontrib.cpptasks; | |
import java.io.BufferedWriter; | |
import java.io.File; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
import java.io.OutputStreamWriter; | |
import java.io.UnsupportedEncodingException; | |
import java.util.Enumeration; | |
import java.util.Hashtable; | |
import java.util.Vector; | |
import javax.xml.parsers.ParserConfigurationException; | |
import javax.xml.parsers.SAXParser; | |
import javax.xml.parsers.SAXParserFactory; | |
import net.sf.antcontrib.cpptasks.compiler.CompilerConfiguration; | |
import org.apache.tools.ant.BuildException; | |
import org.apache.tools.ant.Project; | |
import org.xml.sax.Attributes; | |
import org.xml.sax.SAXException; | |
import org.xml.sax.helpers.DefaultHandler; | |
/** | |
* @author Curt Arnold | |
*/ | |
public final class DependencyTable { | |
/** | |
* This class handles populates the TargetHistory hashtable in response to | |
* SAX parse events | |
*/ | |
private class DependencyTableHandler extends DefaultHandler { | |
private File baseDir; | |
private final DependencyTable dependencyTable; | |
private String includePath; | |
private Vector includes; | |
private String source; | |
private long sourceLastModified; | |
private Vector sysIncludes; | |
/** | |
* Constructor | |
* | |
* @param history | |
* hashtable of TargetHistory keyed by output name | |
* @param outputFiles | |
* existing files in output directory | |
*/ | |
private DependencyTableHandler(DependencyTable dependencyTable, | |
File baseDir) { | |
this.dependencyTable = dependencyTable; | |
this.baseDir = baseDir; | |
includes = new Vector(); | |
sysIncludes = new Vector(); | |
source = null; | |
} | |
public void endElement(String namespaceURI, String localName, | |
String qName) throws SAXException { | |
// | |
// if </source> then | |
// create Dependency object and add to hashtable | |
// if corresponding source file exists and | |
// has the same timestamp | |
// | |
if (qName.equals("source")) { | |
if (source != null && includePath != null) { | |
File existingFile = new File(baseDir, source); | |
// | |
// if the file exists and the time stamp is right | |
// preserve the dependency info | |
if (existingFile.exists()) { | |
// | |
// would have expected exact matches | |
// but was seeing some unexpected difference by | |
// a few tens of milliseconds, as long | |
// as the times are within a second | |
long existingLastModified = existingFile.lastModified(); | |
long diff = existingLastModified - sourceLastModified; | |
if (diff >= -500 && diff <= 500) { | |
DependencyInfo dependInfo = new DependencyInfo( | |
includePath, source, sourceLastModified, | |
includes, sysIncludes); | |
dependencyTable.putDependencyInfo(source, | |
dependInfo); | |
} | |
} | |
source = null; | |
includes.setSize(0); | |
} | |
} else { | |
// | |
// this causes any <source> elements outside the | |
// scope of an <includePath> to be discarded | |
// | |
if (qName.equals("includePath")) { | |
includePath = null; | |
} | |
} | |
} | |
/** | |
* startElement handler | |
*/ | |
public void startElement(String namespaceURI, String localName, | |
String qName, Attributes atts) throws SAXException { | |
// | |
// if includes, then add relative file name to vector | |
// | |
if (qName.equals("include")) { | |
includes.addElement(atts.getValue("file")); | |
} else { | |
if (qName.equals("sysinclude")) { | |
sysIncludes.addElement(atts.getValue("file")); | |
} else { | |
// | |
// if source then | |
// capture source file name, | |
// modification time and reset includes vector | |
// | |
if (qName.equals("source")) { | |
source = atts.getValue("file"); | |
sourceLastModified = Long.parseLong(atts | |
.getValue("lastModified"), 16); | |
includes.setSize(0); | |
sysIncludes.setSize(0); | |
} else { | |
if (qName.equals("includePath")) { | |
includePath = atts.getValue("signature"); | |
} | |
} | |
} | |
} | |
} | |
} | |
public abstract class DependencyVisitor { | |
/** | |
* Previews all the children of this source file. | |
* | |
* May be called multiple times as DependencyInfo's for children are | |
* filled in. | |
* | |
* @return true to continue towards recursion into included files | |
*/ | |
public abstract boolean preview(DependencyInfo parent, | |
DependencyInfo[] children); | |
/** | |
* Called if the dependency depth exhausted the stack. | |
*/ | |
public abstract void stackExhausted(); | |
/** | |
* Visits the dependency info. | |
* | |
* @returns true to continue towards recursion into included files | |
*/ | |
public abstract boolean visit(DependencyInfo dependInfo); | |
} | |
public class TimestampChecker extends DependencyVisitor { | |
private boolean noNeedToRebuild; | |
private long outputLastModified; | |
private boolean rebuildOnStackExhaustion; | |
public TimestampChecker(final long outputLastModified, | |
boolean rebuildOnStackExhaustion) { | |
this.outputLastModified = outputLastModified; | |
noNeedToRebuild = true; | |
this.rebuildOnStackExhaustion = rebuildOnStackExhaustion; | |
} | |
public boolean getMustRebuild() { | |
return !noNeedToRebuild; | |
} | |
public boolean preview(DependencyInfo parent, DependencyInfo[] children) { | |
int withCompositeTimes = 0; | |
long parentCompositeLastModified = parent.getSourceLastModified(); | |
for (int i = 0; i < children.length; i++) { | |
if (children[i] != null) { | |
// | |
// expedient way to determine if a child forces us to | |
// rebuild | |
// | |
visit(children[i]); | |
long childCompositeLastModified = children[i] | |
.getCompositeLastModified(); | |
if (childCompositeLastModified != Long.MIN_VALUE) { | |
withCompositeTimes++; | |
if (childCompositeLastModified > parentCompositeLastModified) { | |
parentCompositeLastModified = childCompositeLastModified; | |
} | |
} | |
} | |
} | |
if (withCompositeTimes == children.length) { | |
parent.setCompositeLastModified(parentCompositeLastModified); | |
} | |
// | |
// may have been changed by an earlier call to visit() | |
// | |
return noNeedToRebuild; | |
} | |
public void stackExhausted() { | |
if (rebuildOnStackExhaustion) { | |
noNeedToRebuild = false; | |
} | |
} | |
public boolean visit(DependencyInfo dependInfo) { | |
if (noNeedToRebuild) { | |
if (dependInfo.getSourceLastModified() > outputLastModified | |
|| dependInfo.getCompositeLastModified() > outputLastModified) { | |
noNeedToRebuild = false; | |
} | |
} | |
// | |
// only need to process the children if | |
// it has not yet been determined whether | |
// we need to rebuild and the composite modified time | |
// has not been determined for this file | |
return noNeedToRebuild | |
&& dependInfo.getCompositeLastModified() == Long.MIN_VALUE; | |
} | |
} | |
private/* final */File baseDir; | |
private String baseDirPath; | |
/** | |
* a hashtable of DependencyInfo[] keyed by output file name | |
*/ | |
private final Hashtable dependencies = new Hashtable(); | |
/** The file the cache was loaded from. */ | |
private/* final */File dependenciesFile; | |
/** Flag indicating whether the cache should be written back to file. */ | |
private boolean dirty; | |
/** | |
* Creates a target history table from dependencies.xml in the prject | |
* directory, if it exists. Otherwise, initializes the dependencies empty. | |
* | |
* @param task | |
* task used for logging history load errors | |
* @param baseDir | |
* output directory for task | |
*/ | |
public DependencyTable(File baseDir) { | |
if (baseDir == null) { | |
throw new NullPointerException("baseDir"); | |
} | |
this.baseDir = baseDir; | |
try { | |
baseDirPath = baseDir.getCanonicalPath(); | |
} catch (IOException ex) { | |
baseDirPath = baseDir.toString(); | |
} | |
dirty = false; | |
// | |
// load any existing dependencies from file | |
dependenciesFile = new File(baseDir, "dependencies.xml"); | |
} | |
public void commit(CCTask task) { | |
// | |
// if not dirty, no need to update file | |
// | |
if (dirty) { | |
// | |
// walk through dependencies to get vector of include paths | |
// identifiers | |
// | |
Vector includePaths = getIncludePaths(); | |
// | |
// | |
// write dependency file | |
// | |
try { | |
FileOutputStream outStream = new FileOutputStream( | |
dependenciesFile); | |
OutputStreamWriter streamWriter; | |
// | |
// Early VM's may not have UTF-8 support | |
// fallback to default code page which | |
// "should" be okay unless there are | |
// non ASCII file names | |
String encodingName = "UTF-8"; | |
try { | |
streamWriter = new OutputStreamWriter(outStream, "UTF-8"); | |
} catch (UnsupportedEncodingException ex) { | |
streamWriter = new OutputStreamWriter(outStream); | |
encodingName = streamWriter.getEncoding(); | |
} | |
BufferedWriter writer = new BufferedWriter(streamWriter); | |
writer.write("<?xml version='1.0' encoding='"); | |
writer.write(encodingName); | |
writer.write("'?>\n"); | |
writer.write("<dependencies>\n"); | |
StringBuffer buf = new StringBuffer(); | |
Enumeration includePathEnum = includePaths.elements(); | |
while (includePathEnum.hasMoreElements()) { | |
writeIncludePathDependencies((String) includePathEnum | |
.nextElement(), writer, buf); | |
} | |
writer.write("</dependencies>\n"); | |
writer.close(); | |
dirty = false; | |
} catch (IOException ex) { | |
task.log("Error writing " + dependenciesFile.toString() + ":" | |
+ ex.toString()); | |
} | |
} | |
} | |
/** | |
* Returns an enumerator of DependencyInfo's | |
*/ | |
public Enumeration elements() { | |
return dependencies.elements(); | |
} | |
/** | |
* This method returns a DependencyInfo for the specific source file and | |
* include path identifier | |
* | |
*/ | |
public DependencyInfo getDependencyInfo(String sourceRelativeName, | |
String includePathIdentifier) { | |
DependencyInfo dependInfo = null; | |
DependencyInfo[] dependInfos = (DependencyInfo[]) dependencies | |
.get(sourceRelativeName); | |
if (dependInfos != null) { | |
for (int i = 0; i < dependInfos.length; i++) { | |
dependInfo = dependInfos[i]; | |
if (dependInfo.getIncludePathIdentifier().equals( | |
includePathIdentifier)) { | |
return dependInfo; | |
} | |
} | |
} | |
return null; | |
} | |
private Vector getIncludePaths() { | |
Vector includePaths = new Vector(); | |
DependencyInfo[] dependInfos; | |
Enumeration dependenciesEnum = dependencies.elements(); | |
while (dependenciesEnum.hasMoreElements()) { | |
dependInfos = (DependencyInfo[]) dependenciesEnum.nextElement(); | |
for (int i = 0; i < dependInfos.length; i++) { | |
DependencyInfo dependInfo = dependInfos[i]; | |
boolean matchesExisting = false; | |
final String dependIncludePath = dependInfo | |
.getIncludePathIdentifier(); | |
Enumeration includePathEnum = includePaths.elements(); | |
while (includePathEnum.hasMoreElements()) { | |
if (dependIncludePath.equals(includePathEnum.nextElement())) { | |
matchesExisting = true; | |
break; | |
} | |
} | |
if (!matchesExisting) { | |
includePaths.addElement(dependIncludePath); | |
} | |
} | |
} | |
return includePaths; | |
} | |
public void load() throws IOException, ParserConfigurationException, | |
SAXException { | |
dependencies.clear(); | |
if (dependenciesFile.exists()) { | |
SAXParserFactory factory = SAXParserFactory.newInstance(); | |
factory.setValidating(false); | |
SAXParser parser = factory.newSAXParser(); | |
parser.parse(dependenciesFile, new DependencyTableHandler(this, | |
baseDir)); | |
dirty = false; | |
} | |
} | |
/** | |
* Determines if the specified target needs to be rebuilt. | |
* | |
* This task may result in substantial IO as files are parsed to determine | |
* their dependencies | |
*/ | |
public boolean needsRebuild(CCTask task, TargetInfo target, | |
int dependencyDepth) { | |
// look at any files where the compositeLastModified | |
// is not known, but the includes are known | |
// | |
boolean mustRebuild = false; | |
CompilerConfiguration compiler = (CompilerConfiguration) target | |
.getConfiguration(); | |
String includePathIdentifier = compiler.getIncludePathIdentifier(); | |
File[] sources = target.getSources(); | |
DependencyInfo[] dependInfos = new DependencyInfo[sources.length]; | |
long outputLastModified = target.getOutput().lastModified(); | |
// | |
// try to solve problem using existing dependency info | |
// (not parsing any new files) | |
// | |
DependencyInfo[] stack = new DependencyInfo[50]; | |
boolean rebuildOnStackExhaustion = true; | |
if (dependencyDepth >= 0) { | |
if (dependencyDepth < 50) { | |
stack = new DependencyInfo[dependencyDepth]; | |
} | |
rebuildOnStackExhaustion = false; | |
} | |
TimestampChecker checker = new TimestampChecker(outputLastModified, | |
rebuildOnStackExhaustion); | |
for (int i = 0; i < sources.length && !mustRebuild; i++) { | |
File source = sources[i]; | |
String relative = CUtil.getRelativePath(baseDirPath, source); | |
DependencyInfo dependInfo = getDependencyInfo(relative, | |
includePathIdentifier); | |
if (dependInfo == null) { | |
task.log("Parsing " + relative, Project.MSG_VERBOSE); | |
dependInfo = parseIncludes(task, compiler, source); | |
} | |
walkDependencies(task, dependInfo, compiler, stack, checker); | |
mustRebuild = checker.getMustRebuild(); | |
} | |
return mustRebuild; | |
} | |
public DependencyInfo parseIncludes(CCTask task, | |
CompilerConfiguration compiler, File source) { | |
DependencyInfo dependInfo = compiler.parseIncludes(task, baseDir, | |
source); | |
String relativeSource = CUtil.getRelativePath(baseDirPath, source); | |
putDependencyInfo(relativeSource, dependInfo); | |
return dependInfo; | |
} | |
private void putDependencyInfo(String key, DependencyInfo dependInfo) { | |
// | |
// optimistic, add new value | |
// | |
DependencyInfo[] old = (DependencyInfo[]) dependencies.put(key, | |
new DependencyInfo[]{dependInfo}); | |
dirty = true; | |
// | |
// something was already there | |
// | |
if (old != null) { | |
// | |
// see if the include path matches a previous entry | |
// if so replace it | |
String includePathIdentifier = dependInfo | |
.getIncludePathIdentifier(); | |
for (int i = 0; i < old.length; i++) { | |
DependencyInfo oldDepend = old[i]; | |
if (oldDepend.getIncludePathIdentifier().equals( | |
includePathIdentifier)) { | |
old[i] = dependInfo; | |
dependencies.put(key, old); | |
return; | |
} | |
} | |
// | |
// no match prepend the new entry to the array | |
// of dependencies for the file | |
DependencyInfo[] combined = new DependencyInfo[old.length + 1]; | |
combined[0] = dependInfo; | |
for (int i = 0; i < old.length; i++) { | |
combined[i + 1] = old[i]; | |
} | |
dependencies.put(key, combined); | |
} | |
return; | |
} | |
public void walkDependencies(CCTask task, DependencyInfo dependInfo, | |
CompilerConfiguration compiler, DependencyInfo[] stack, | |
DependencyVisitor visitor) throws BuildException { | |
// | |
// visit this node | |
// if visit returns true then | |
// visit the referenced include and sysInclude dependencies | |
// | |
if (visitor.visit(dependInfo)) { | |
// | |
// find first null entry on stack | |
// | |
int stackPosition = -1; | |
for (int i = 0; i < stack.length; i++) { | |
if (stack[i] == null) { | |
stackPosition = i; | |
stack[i] = dependInfo; | |
break; | |
} else { | |
// | |
// if we have appeared early in the calling history | |
// then we didn't exceed the criteria | |
if (stack[i] == dependInfo) { | |
return; | |
} | |
} | |
} | |
if (stackPosition == -1) { | |
visitor.stackExhausted(); | |
return; | |
} | |
// | |
// locate dependency infos | |
// | |
String[] includes = dependInfo.getIncludes(); | |
String includePathIdentifier = compiler.getIncludePathIdentifier(); | |
DependencyInfo[] includeInfos = new DependencyInfo[includes.length]; | |
for (int i = 0; i < includes.length; i++) { | |
DependencyInfo includeInfo = getDependencyInfo(includes[i], | |
includePathIdentifier); | |
includeInfos[i] = includeInfo; | |
} | |
// | |
// preview with only the already available dependency infos | |
// | |
if (visitor.preview(dependInfo, includeInfos)) { | |
// | |
// now need to fill in the missing DependencyInfos | |
// | |
int missingCount = 0; | |
for (int i = 0; i < includes.length; i++) { | |
if (includeInfos[i] == null) { | |
missingCount++; | |
task.log("Parsing " + includes[i], Project.MSG_VERBOSE); | |
// If the include is part of a UNC don't go building a | |
// relative file name. | |
File src = includes[i].startsWith("\\\\") ? new File( | |
includes[i]) : new File(baseDir, includes[i]); | |
DependencyInfo includeInfo = parseIncludes(task, | |
compiler, src); | |
includeInfos[i] = includeInfo; | |
} | |
} | |
// | |
// if it passes a review the second time | |
// then recurse into all the children | |
if (missingCount == 0 | |
|| visitor.preview(dependInfo, includeInfos)) { | |
// | |
// recurse into | |
// | |
for (int i = 0; i < includeInfos.length; i++) { | |
DependencyInfo includeInfo = includeInfos[i]; | |
walkDependencies(task, includeInfo, compiler, stack, | |
visitor); | |
} | |
} | |
} | |
stack[stackPosition] = null; | |
} | |
} | |
private void writeDependencyInfo(BufferedWriter writer, StringBuffer buf, | |
DependencyInfo dependInfo) throws IOException { | |
String[] includes = dependInfo.getIncludes(); | |
String[] sysIncludes = dependInfo.getSysIncludes(); | |
// | |
// if the includes have not been evaluted then | |
// it is not worth our time saving it | |
// and trying to distiguish between files with | |
// no dependencies and those with undetermined dependencies | |
buf.setLength(0); | |
buf.append(" <source file=\""); | |
buf.append(CUtil.xmlAttribEncode(dependInfo.getSource())); | |
buf.append("\" lastModified=\""); | |
buf.append(Long.toHexString(dependInfo.getSourceLastModified())); | |
buf.append("\">\n"); | |
writer.write(buf.toString()); | |
for (int i = 0; i < includes.length; i++) { | |
buf.setLength(0); | |
buf.append(" <include file=\""); | |
buf.append(CUtil.xmlAttribEncode(includes[i])); | |
buf.append("\"/>\n"); | |
writer.write(buf.toString()); | |
} | |
for (int i = 0; i < sysIncludes.length; i++) { | |
buf.setLength(0); | |
buf.append(" <sysinclude file=\""); | |
buf.append(CUtil.xmlAttribEncode(sysIncludes[i])); | |
buf.append("\"/>\n"); | |
writer.write(buf.toString()); | |
} | |
writer.write(" </source>\n"); | |
return; | |
} | |
private void writeIncludePathDependencies(String includePathIdentifier, | |
BufferedWriter writer, StringBuffer buf) throws IOException { | |
// | |
// include path element | |
// | |
buf.setLength(0); | |
buf.append(" <includePath signature=\""); | |
buf.append(CUtil.xmlAttribEncode(includePathIdentifier)); | |
buf.append("\">\n"); | |
writer.write(buf.toString()); | |
Enumeration dependenciesEnum = dependencies.elements(); | |
while (dependenciesEnum.hasMoreElements()) { | |
DependencyInfo[] dependInfos = (DependencyInfo[]) dependenciesEnum | |
.nextElement(); | |
for (int i = 0; i < dependInfos.length; i++) { | |
DependencyInfo dependInfo = dependInfos[i]; | |
// | |
// if this is for the same include path | |
// then output the info | |
if (dependInfo.getIncludePathIdentifier().equals( | |
includePathIdentifier)) { | |
writeDependencyInfo(writer, buf, dependInfo); | |
} | |
} | |
} | |
writer.write(" </includePath>\n"); | |
} | |
} |