/* * #%L * Native ARchive plugin for Maven * %% * Copyright (C) 2002 - 2014 NAR Maven Plugin developers. * %% * 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. * #L% */ package com.github.maven_nar.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 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; import com.github.maven_nar.cpptasks.compiler.CompilerConfiguration; /** * @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 final File baseDir; private final DependencyTable dependencyTable; private String includePath; private final Vector includes; private String source; private long sourceLastModified; private final Vector sysIncludes; /** * Constructor * * @param history * hashtable of TargetHistory keyed by output name * @param outputFiles * existing files in output directory */ private DependencyTableHandler(final DependencyTable dependencyTable, final File baseDir) { this.dependencyTable = dependencyTable; this.baseDir = baseDir; this.includes = new Vector(); this.sysIncludes = new Vector(); this.source = null; } @Override public void endElement(final String namespaceURI, final String localName, final 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 (this.source != null && this.includePath != null) { final File existingFile = new File(this.baseDir, this.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 final long existingLastModified = existingFile.lastModified(); if (!CUtil.isSignificantlyAfter(existingLastModified, this.sourceLastModified) && !CUtil.isSignificantlyBefore(existingLastModified, this.sourceLastModified)) { final DependencyInfo dependInfo = new DependencyInfo(this.includePath, this.source, this.sourceLastModified, this.includes, this.sysIncludes); this.dependencyTable.putDependencyInfo(this.source, dependInfo); } } this.source = null; this.includes.setSize(0); } } else { // // this causes any <source> elements outside the // scope of an <includePath> to be discarded // if (qName.equals("includePath")) { this.includePath = null; } } } /** * startElement handler */ @Override public void startElement(final String namespaceURI, final String localName, final String qName, final Attributes atts) throws SAXException { // // if includes, then add relative file name to vector // if (qName.equals("include")) { this.includes.addElement(atts.getValue("file")); } else { if (qName.equals("sysinclude")) { this.sysIncludes.addElement(atts.getValue("file")); } else { // // if source then // capture source file name, // modification time and reset includes vector // if (qName.equals("source")) { this.source = atts.getValue("file"); this.sourceLastModified = Long.parseLong(atts.getValue("lastModified"), 16); this.includes.setSize(0); this.sysIncludes.setSize(0); } else { if (qName.equals("includePath")) { this.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. * * @return true to continue towards recursion into included files */ public abstract boolean visit(DependencyInfo dependInfo); } public class TimestampChecker extends DependencyVisitor { private boolean noNeedToRebuild; private final long outputLastModified; private final boolean rebuildOnStackExhaustion; public TimestampChecker(final long outputLastModified, final boolean rebuildOnStackExhaustion) { this.outputLastModified = outputLastModified; this.noNeedToRebuild = true; this.rebuildOnStackExhaustion = rebuildOnStackExhaustion; } public boolean getMustRebuild() { return !this.noNeedToRebuild; } @Override public boolean preview(final DependencyInfo parent, final DependencyInfo[] children) { // BEGINFREEHEP // int withCompositeTimes = 0; // long parentCompositeLastModified = parent.getSourceLastModified(); // ENDFREEHEP for (final DependencyInfo element : children) { if (element != null) { // // expedient way to determine if a child forces us to // rebuild // visit(element); // BEGINFREEHEP // long childCompositeLastModified = children[i] // .getCompositeLastModified(); // if (childCompositeLastModified != Long.MIN_VALUE) { // withCompositeTimes++; // if (childCompositeLastModified > parentCompositeLastModified) { // parentCompositeLastModified = childCompositeLastModified; // } // } // ENDFREEHEP } } // BEGINFREEHEP // if (withCompositeTimes == children.length) { // parent.setCompositeLastModified(parentCompositeLastModified); // } // ENDFREEHEP // // may have been changed by an earlier call to visit() // return this.noNeedToRebuild; } @Override public void stackExhausted() { if (this.rebuildOnStackExhaustion) { this.noNeedToRebuild = false; } } @Override public boolean visit(final DependencyInfo dependInfo) { if (this.noNeedToRebuild && CUtil.isSignificantlyAfter(dependInfo.getSourceLastModified(), this.outputLastModified)) { // FREEHEP // || // CUtil.isSignificantlyAfter(dependInfo.getCompositeLastModified(), // outputLastModified)) { this.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 this.noNeedToRebuild; // FREEHEP // && dependInfo.getCompositeLastModified() == Long.MIN_VALUE; } } private final/* 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/* 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 baseDir * output directory for task */ public DependencyTable(final File baseDir) { if (baseDir == null) { throw new NullPointerException("baseDir"); } this.baseDir = baseDir; try { this.baseDirPath = baseDir.getCanonicalPath(); } catch (final IOException ex) { this.baseDirPath = baseDir.toString(); } this.dirty = false; // // load any existing dependencies from file this.dependenciesFile = new File(baseDir, "dependencies.xml"); } public void commit(final CCTask task) { // // if not dirty, no need to update file // if (this.dirty) { // // walk through dependencies to get vector of include paths // identifiers // final Vector includePaths = getIncludePaths(); // // // write dependency file // try { final FileOutputStream outStream = new FileOutputStream(this.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 (final UnsupportedEncodingException ex) { streamWriter = new OutputStreamWriter(outStream); encodingName = streamWriter.getEncoding(); } final BufferedWriter writer = new BufferedWriter(streamWriter); writer.write("<?xml version='1.0' encoding='"); writer.write(encodingName); writer.write("'?>\n"); writer.write("<dependencies>\n"); final StringBuffer buf = new StringBuffer(); final Enumeration includePathEnum = includePaths.elements(); while (includePathEnum.hasMoreElements()) { writeIncludePathDependencies((String) includePathEnum.nextElement(), writer, buf); } writer.write("</dependencies>\n"); writer.close(); this.dirty = false; } catch (final IOException ex) { task.log("Error writing " + this.dependenciesFile.toString() + ":" + ex.toString()); } } } /** * Returns an enumerator of DependencyInfo's */ public Enumeration elements() { return this.dependencies.elements(); } /** * This method returns a DependencyInfo for the specific source file and * include path identifier * */ public DependencyInfo getDependencyInfo(final String sourceRelativeName, final String includePathIdentifier) { DependencyInfo dependInfo = null; final DependencyInfo[] dependInfos = (DependencyInfo[]) this.dependencies.get(sourceRelativeName); if (dependInfos != null) { for (final DependencyInfo dependInfo2 : dependInfos) { dependInfo = dependInfo2; if (dependInfo.getIncludePathIdentifier().equals(includePathIdentifier)) { return dependInfo; } } } return null; } private Vector getIncludePaths() { final Vector includePaths = new Vector(); DependencyInfo[] dependInfos; final Enumeration dependenciesEnum = this.dependencies.elements(); while (dependenciesEnum.hasMoreElements()) { dependInfos = (DependencyInfo[]) dependenciesEnum.nextElement(); for (final DependencyInfo dependInfo : dependInfos) { boolean matchesExisting = false; final String dependIncludePath = dependInfo.getIncludePathIdentifier(); final 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 { this.dependencies.clear(); if (this.dependenciesFile.exists()) { final SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setValidating(false); final SAXParser parser = factory.newSAXParser(); parser.parse(this.dependenciesFile, new DependencyTableHandler(this, this.baseDir)); this.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(final CCTask task, final TargetInfo target, final int dependencyDepth) { // look at any files where the compositeLastModified // is not known, but the includes are known // boolean mustRebuild = false; final CompilerConfiguration compiler = (CompilerConfiguration) target.getConfiguration(); final String includePathIdentifier = compiler.getIncludePathIdentifier(); final File[] sources = target.getSources(); final DependencyInfo[] dependInfos = new DependencyInfo[sources.length]; final 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; } final TimestampChecker checker = new TimestampChecker(outputLastModified, rebuildOnStackExhaustion); for (int i = 0; i < sources.length && !mustRebuild; i++) { final File source = sources[i]; final String relative = CUtil.getRelativePath(this.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(final CCTask task, final CompilerConfiguration compiler, final File source) { final DependencyInfo dependInfo = compiler.parseIncludes(task, this.baseDir, source); final String relativeSource = CUtil.getRelativePath(this.baseDirPath, source); putDependencyInfo(relativeSource, dependInfo); return dependInfo; } private void putDependencyInfo(final String key, final DependencyInfo dependInfo) { // // optimistic, add new value // final DependencyInfo[] old = (DependencyInfo[]) this.dependencies.put(key, new DependencyInfo[] { dependInfo }); this.dirty = true; // // something was already there // if (old != null) { // // see if the include path matches a previous entry // if so replace it final String includePathIdentifier = dependInfo.getIncludePathIdentifier(); for (int i = 0; i < old.length; i++) { final DependencyInfo oldDepend = old[i]; if (oldDepend.getIncludePathIdentifier().equals(includePathIdentifier)) { old[i] = dependInfo; this.dependencies.put(key, old); return; } } // // no match prepend the new entry to the array // of dependencies for the file final DependencyInfo[] combined = new DependencyInfo[old.length + 1]; combined[0] = dependInfo; System.arraycopy(old, 0, combined, 1, old.length); this.dependencies.put(key, combined); } return; } public void walkDependencies(final CCTask task, final DependencyInfo dependInfo, final CompilerConfiguration compiler, final DependencyInfo[] stack, final DependencyVisitor visitor) throws BuildException { // BEGINFREEHEP if (dependInfo.hasTag(visitor)) { return; } dependInfo.setTag(visitor); // ENDFREEHEP // // visit this node // if visit returns true then // visit the referenced include and sysInclude dependencies // if (visitor.visit(dependInfo)) { // BEGINFREEHEP // // // // 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; // } // ENDFREEHEP // // locate dependency infos // final String[] includes = dependInfo.getIncludes(); final String includePathIdentifier = compiler.getIncludePathIdentifier(); final DependencyInfo[] includeInfos = new DependencyInfo[includes.length]; for (int i = 0; i < includes.length; i++) { final 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 filepath is relative // then anchor it the base directory File src = new File(includes[i]); if (!src.isAbsolute()) { src = new File(this.baseDir, includes[i]); } final 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 (final DependencyInfo includeInfo : includeInfos) { // Darren Sargent 23Oct2008 // only recurse for direct includes of current source // file if (includeInfo.getSource().contains(File.separatorChar + "src" + File.separatorChar + "main") || includeInfo.getSource().contains(File.separatorChar + "src" + File.separatorChar + "test")) { task.log("Walking dependencies for " + includeInfo.getSource(), Project.MSG_VERBOSE); walkDependencies(task, includeInfo, compiler, stack, visitor); } } } } // FREEHEP // stack[stackPosition] = null; } } private void writeDependencyInfo(final BufferedWriter writer, final StringBuffer buf, final DependencyInfo dependInfo) throws IOException { final String[] includes = dependInfo.getIncludes(); final 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 (final String include : includes) { buf.setLength(0); buf.append(" <include file=\""); buf.append(CUtil.xmlAttribEncode(include)); buf.append("\"/>\n"); writer.write(buf.toString()); } for (final String sysInclude : sysIncludes) { buf.setLength(0); buf.append(" <sysinclude file=\""); buf.append(CUtil.xmlAttribEncode(sysInclude)); buf.append("\"/>\n"); writer.write(buf.toString()); } writer.write(" </source>\n"); return; } private void writeIncludePathDependencies(final String includePathIdentifier, final BufferedWriter writer, final 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()); final Enumeration dependenciesEnum = this.dependencies.elements(); while (dependenciesEnum.hasMoreElements()) { final DependencyInfo[] dependInfos = (DependencyInfo[]) dependenciesEnum.nextElement(); for (final DependencyInfo dependInfo : dependInfos) { // // 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"); } }