| /* | |
| * | |
| * 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"); | |
| } | |
| } |