/* | |
* | |
* 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.SAXParser; | |
import javax.xml.parsers.SAXParserFactory; | |
import net.sf.antcontrib.cpptasks.compiler.ProcessorConfiguration; | |
import org.apache.tools.ant.BuildException; | |
import org.xml.sax.Attributes; | |
import org.xml.sax.SAXException; | |
import org.xml.sax.helpers.DefaultHandler; | |
/** | |
* A history of the compiler and linker settings used to build the files in the | |
* same directory as the history. | |
* | |
* @author Curt Arnold | |
*/ | |
public final class TargetHistoryTable { | |
/** | |
* This class handles populates the TargetHistory hashtable in response to | |
* SAX parse events | |
*/ | |
private class TargetHistoryTableHandler extends DefaultHandler { | |
private final File baseDir; | |
private String config; | |
private final Hashtable history; | |
private String output; | |
private long outputLastModified; | |
private final Vector sources = new Vector(); | |
/** | |
* Constructor | |
* | |
* @param history | |
* hashtable of TargetHistory keyed by output name | |
* @param outputFiles | |
* existing files in output directory | |
*/ | |
private TargetHistoryTableHandler(Hashtable history, File baseDir) { | |
this.history = history; | |
config = null; | |
output = null; | |
this.baseDir = baseDir; | |
} | |
public void endElement(String namespaceURI, String localName, | |
String qName) throws SAXException { | |
// | |
// if </target> then | |
// create TargetHistory object and add to hashtable | |
// if corresponding output file exists and | |
// has the same timestamp | |
// | |
if (qName.equals("target")) { | |
if (config != null && output != null) { | |
File existingFile = new File(baseDir, output); | |
// | |
// if the corresponding files doesn't exist or has a | |
// different | |
// modification time, then discard this record | |
if (existingFile.exists()) { | |
long existingLastModified = existingFile.lastModified(); | |
// | |
// would have expected exact time stamps | |
// but have observed slight differences | |
// in return value for multiple evaluations of | |
// lastModified(). Check if times are within | |
// a second | |
long diff = outputLastModified - existingLastModified; | |
if (diff >= -500 && diff <= 500) { | |
SourceHistory[] sourcesArray = new SourceHistory[sources | |
.size()]; | |
sources.copyInto(sourcesArray); | |
TargetHistory targetHistory = new TargetHistory( | |
config, output, outputLastModified, | |
sourcesArray); | |
history.put(output, targetHistory); | |
} | |
} | |
} | |
output = null; | |
sources.setSize(0); | |
} else { | |
// | |
// reset config so targets not within a processor element | |
// don't pick up a previous processors signature | |
// | |
if (qName.equals("processor")) { | |
config = null; | |
} | |
} | |
} | |
/** | |
* startElement handler | |
*/ | |
public void startElement(String namespaceURI, String localName, | |
String qName, Attributes atts) throws SAXException { | |
// | |
// if sourceElement | |
// | |
if (qName.equals("source")) { | |
String sourceFile = atts.getValue("file"); | |
long sourceLastModified = Long.parseLong(atts | |
.getValue("lastModified"), 16); | |
sources.addElement(new SourceHistory(sourceFile, | |
sourceLastModified)); | |
} else { | |
// | |
// if <target> element, | |
// grab file name and lastModified values | |
// TargetHistory object will be created in endElement | |
// | |
if (qName.equals("target")) { | |
sources.setSize(0); | |
output = atts.getValue("file"); | |
outputLastModified = Long.parseLong(atts | |
.getValue("lastModified"), 16); | |
} else { | |
// | |
// if <processor> element, | |
// grab signature attribute | |
// | |
if (qName.equals("processor")) { | |
config = atts.getValue("signature"); | |
} | |
} | |
} | |
} | |
} | |
/** Flag indicating whether the cache should be written back to file. */ | |
private boolean dirty; | |
/** | |
* a hashtable of TargetHistory's keyed by output file name | |
*/ | |
private final Hashtable history = new Hashtable(); | |
/** The file the cache was loaded from. */ | |
private/* final */File historyFile; | |
private/* final */File outputDir; | |
private String outputDirPath; | |
/** | |
* Creates a target history table from history.xml in the output directory, | |
* if it exists. Otherwise, initializes the history table empty. | |
* | |
* @param task | |
* task used for logging history load errors | |
* @param outputDir | |
* output directory for task | |
*/ | |
public TargetHistoryTable(CCTask task, File outputDir) | |
throws BuildException { | |
if (outputDir == null) { | |
throw new NullPointerException("outputDir"); | |
} | |
if (!outputDir.isDirectory()) { | |
throw new BuildException("Output directory is not a directory"); | |
} | |
if (!outputDir.exists()) { | |
throw new BuildException("Output directory does not exist"); | |
} | |
this.outputDir = outputDir; | |
try { | |
outputDirPath = outputDir.getCanonicalPath(); | |
} catch (IOException ex) { | |
outputDirPath = outputDir.toString(); | |
} | |
// | |
// load any existing history from file | |
// suppressing any records whose corresponding | |
// file does not exist, is zero-length or | |
// last modified dates differ | |
historyFile = new File(outputDir, "history.xml"); | |
if (historyFile.exists()) { | |
SAXParserFactory factory = SAXParserFactory.newInstance(); | |
factory.setValidating(false); | |
try { | |
SAXParser parser = factory.newSAXParser(); | |
parser.parse(historyFile, new TargetHistoryTableHandler( | |
history, outputDir)); | |
} catch (Exception ex) { | |
// | |
// a failure on loading this history is not critical | |
// but should be logged | |
task.log("Error reading history.xml: " + ex.toString()); | |
} | |
} else { | |
// | |
// create empty history file for identifying new files by last | |
// modified | |
// timestamp comperation (to compare with | |
// System.currentTimeMillis() don't work on Unix, because it | |
// maesure timestamps only in seconds). | |
// | |
try { | |
FileOutputStream outputStream = new FileOutputStream( | |
historyFile); | |
byte[] historyElement = new byte[]{0x3C, 0x68, 0x69, 0x73, | |
0x74, 0x6F, 0x72, 0x79, 0x2F, 0x3E}; | |
outputStream.write(historyElement); | |
outputStream.close(); | |
} catch (IOException ex) { | |
throw new BuildException("Can't create history file", ex); | |
} | |
} | |
} | |
public void commit() throws IOException { | |
// | |
// if not dirty, no need to update file | |
// | |
if (dirty) { | |
// | |
// build (small) hashtable of config id's in history | |
// | |
Hashtable configs = new Hashtable(20); | |
Enumeration elements = history.elements(); | |
while (elements.hasMoreElements()) { | |
TargetHistory targetHistory = (TargetHistory) elements | |
.nextElement(); | |
String configId = targetHistory.getProcessorConfiguration(); | |
if (configs.get(configId) == null) { | |
configs.put(configId, configId); | |
} | |
} | |
FileOutputStream outStream = new FileOutputStream(historyFile); | |
OutputStreamWriter outWriter; | |
// | |
// early VM's don't support UTF-8 encoding | |
// try and fallback to the default encoding | |
// otherwise | |
String encodingName = "UTF-8"; | |
try { | |
outWriter = new OutputStreamWriter(outStream, "UTF-8"); | |
} catch (UnsupportedEncodingException ex) { | |
outWriter = new OutputStreamWriter(outStream); | |
encodingName = outWriter.getEncoding(); | |
} | |
BufferedWriter writer = new BufferedWriter(outWriter); | |
writer.write("<?xml version='1.0' encoding='"); | |
writer.write(encodingName); | |
writer.write("'?>\n"); | |
writer.write("<history>\n"); | |
StringBuffer buf = new StringBuffer(200); | |
Enumeration configEnum = configs.elements(); | |
while (configEnum.hasMoreElements()) { | |
String configId = (String) configEnum.nextElement(); | |
buf.setLength(0); | |
buf.append(" <processor signature=\""); | |
buf.append(CUtil.xmlAttribEncode(configId)); | |
buf.append("\">\n"); | |
writer.write(buf.toString()); | |
elements = history.elements(); | |
while (elements.hasMoreElements()) { | |
TargetHistory targetHistory = (TargetHistory) elements | |
.nextElement(); | |
if (targetHistory.getProcessorConfiguration().equals( | |
configId)) { | |
buf.setLength(0); | |
buf.append(" <target file=\""); | |
buf.append(CUtil.xmlAttribEncode(targetHistory | |
.getOutput())); | |
buf.append("\" lastModified=\""); | |
buf.append(Long.toHexString(targetHistory | |
.getOutputLastModified())); | |
buf.append("\">\n"); | |
writer.write(buf.toString()); | |
SourceHistory[] sourceHistories = targetHistory | |
.getSources(); | |
for (int i = 0; i < sourceHistories.length; i++) { | |
buf.setLength(0); | |
buf.append(" <source file=\""); | |
buf.append(CUtil.xmlAttribEncode(sourceHistories[i] | |
.getRelativePath())); | |
buf.append("\" lastModified=\""); | |
buf.append(Long.toHexString(sourceHistories[i] | |
.getLastModified())); | |
buf.append("\"/>\n"); | |
writer.write(buf.toString()); | |
} | |
writer.write(" </target>\n"); | |
} | |
} | |
writer.write(" </processor>\n"); | |
} | |
writer.write("</history>\n"); | |
writer.close(); | |
dirty = false; | |
} | |
} | |
public TargetHistory get(String configId, String outputName) { | |
TargetHistory targetHistory = (TargetHistory) history.get(outputName); | |
if (targetHistory != null) { | |
if (!targetHistory.getProcessorConfiguration().equals(configId)) { | |
targetHistory = null; | |
} | |
} | |
return targetHistory; | |
} | |
public void markForRebuild(Hashtable targetInfos) { | |
Enumeration targetInfoEnum = targetInfos.elements(); | |
while (targetInfoEnum.hasMoreElements()) { | |
markForRebuild((TargetInfo) targetInfoEnum.nextElement()); | |
} | |
} | |
public void markForRebuild(TargetInfo targetInfo) { | |
// | |
// if it must already be rebuilt, no need to check further | |
// | |
if (!targetInfo.getRebuild()) { | |
TargetHistory history = get(targetInfo.getConfiguration() | |
.toString(), targetInfo.getOutput().getName()); | |
if (history == null) { | |
targetInfo.mustRebuild(); | |
} else { | |
SourceHistory[] sourceHistories = history.getSources(); | |
File[] sources = targetInfo.getSources(); | |
if (sourceHistories.length != sources.length) { | |
targetInfo.mustRebuild(); | |
} else { | |
for (int i = 0; i < sourceHistories.length | |
&& !targetInfo.getRebuild(); i++) { | |
// | |
// relative file name, must absolutize it on output | |
// directory | |
// | |
boolean foundMatch = false; | |
String historySourcePath = sourceHistories[i] | |
.getAbsolutePath(outputDir); | |
for (int j = 0; j < sources.length; j++) { | |
File targetSource = sources[j]; | |
String targetSourcePath = targetSource | |
.getAbsolutePath(); | |
if (targetSourcePath.equals(historySourcePath)) { | |
foundMatch = true; | |
if (targetSource.lastModified() != sourceHistories[i] | |
.getLastModified()) { | |
targetInfo.mustRebuild(); | |
break; | |
} | |
} | |
} | |
if (!foundMatch) { | |
targetInfo.mustRebuild(); | |
} | |
} | |
} | |
} | |
} | |
} | |
public void update(ProcessorConfiguration config, String[] sources) { | |
String configId = config.getIdentifier(); | |
String[] onesource = new String[1]; | |
String outputName; | |
for (int i = 0; i < sources.length; i++) { | |
onesource[0] = sources[i]; | |
outputName = config.getOutputFileName(sources[i]); | |
update(configId, outputName, onesource); | |
} | |
} | |
private void update(String configId, String outputName, String[] sources) { | |
File outputFile = new File(outputDir, outputName); | |
// | |
// if output file doesn't exist or predates the start of the | |
// compile step (most likely a compilation error) then | |
// do not write add a history entry | |
// | |
if (outputFile.exists() | |
&& outputFile.lastModified() >= historyFile.lastModified()) { | |
dirty = true; | |
history.remove(outputName); | |
SourceHistory[] sourceHistories = new SourceHistory[sources.length]; | |
for (int i = 0; i < sources.length; i++) { | |
File sourceFile = new File(sources[i]); | |
long lastModified = sourceFile.lastModified(); | |
String relativePath = CUtil.getRelativePath(outputDirPath, | |
sourceFile); | |
sourceHistories[i] = new SourceHistory(relativePath, | |
lastModified); | |
} | |
TargetHistory newHistory = new TargetHistory(configId, outputName, | |
outputFile.lastModified(), sourceHistories); | |
history.put(outputName, newHistory); | |
} | |
} | |
public void update(TargetInfo linkTarget) { | |
File outputFile = linkTarget.getOutput(); | |
String outputName = outputFile.getName(); | |
// | |
// if output file doesn't exist or predates the start of the | |
// compile or link step (most likely a compilation error) then | |
// do not write add a history entry | |
// | |
if (outputFile.exists() | |
&& outputFile.lastModified() >= historyFile.lastModified()) { | |
dirty = true; | |
history.remove(outputName); | |
SourceHistory[] sourceHistories = linkTarget | |
.getSourceHistories(outputDirPath); | |
TargetHistory newHistory = new TargetHistory(linkTarget | |
.getConfiguration().getIdentifier(), outputName, outputFile | |
.lastModified(), sourceHistories); | |
history.put(outputName, newHistory); | |
} | |
} | |
} |