/* * #%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.FileWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.util.Enumeration; import java.util.Hashtable; import java.util.Map; import java.util.Vector; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.apache.tools.ant.BuildException; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import com.github.maven_nar.cpptasks.compiler.ProcessorConfiguration; /** * 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<String, TargetHistory> history; private String output; private long outputLastModified; private final Vector<SourceHistory> sources = new Vector<>(); /** * Constructor * * @param history * hashtable of TargetHistory keyed by output name */ private TargetHistoryTableHandler(final Hashtable<String, TargetHistory> history, final File baseDir) { this.history = history; this.config = null; this.output = null; this.baseDir = baseDir; } @Override public void endElement(final String namespaceURI, final String localName, final 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 (this.config != null && this.output != null) { final File existingFile = new File(this.baseDir, this.output); // // if the corresponding files doesn't exist or has a // different // modification time, then discard this record if (existingFile.exists()) { // // 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 final long existingLastModified = existingFile.lastModified(); if (!CUtil.isSignificantlyBefore(existingLastModified, this.outputLastModified) && !CUtil.isSignificantlyAfter(existingLastModified, this.outputLastModified)) { final SourceHistory[] sourcesArray = new SourceHistory[this.sources.size()]; this.sources.copyInto(sourcesArray); final TargetHistory targetHistory = new TargetHistory(this.config, this.output, this.outputLastModified, sourcesArray); this.history.put(this.output, targetHistory); } } } this.output = null; this.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")) { this.config = null; } } } /** * startElement handler */ @Override public void startElement(final String namespaceURI, final String localName, final String qName, final Attributes atts) throws SAXException { // // if sourceElement // if (qName.equals("source")) { final String sourceFile = atts.getValue("file"); final long sourceLastModified = Long.parseLong(atts.getValue("lastModified"), 16); this.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")) { this.sources.setSize(0); this.output = atts.getValue("file"); this.outputLastModified = Long.parseLong(atts.getValue("lastModified"), 16); } else { // // if <processor> element, // grab signature attribute // if (qName.equals("processor")) { this.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<String, TargetHistory> history = new Hashtable<>(); /** * The file the cache was loaded from. */ private final/* final */File historyFile; private final/* 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(final CCTask task, final 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 { this.outputDirPath = outputDir.getCanonicalPath(); } catch (final IOException ex) { this.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 this.historyFile = new File(outputDir, "history.xml"); if (this.historyFile.exists()) { final SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setValidating(false); try { final SAXParser parser = factory.newSAXParser(); parser.parse(this.historyFile, new TargetHistoryTableHandler(this.history, outputDir)); } catch (final 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 { try { final File temp = File.createTempFile("history.xml", Long.toString(System.nanoTime()), outputDir); try (FileWriter writer = new FileWriter(temp)) { writer.write("<history/>"); } if (!temp.renameTo(this.historyFile)) { throw new IOException("Could not rename " + temp + " to " + this.historyFile); } } catch (final 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 (this.dirty) { // // build (small) hashtable of config id's in history // final Hashtable<String, String> configs = new Hashtable<>(20); Enumeration<TargetHistory> elements = this.history.elements(); while (elements.hasMoreElements()) { final TargetHistory targetHistory = elements.nextElement(); final String configId = targetHistory.getProcessorConfiguration(); if (configs.get(configId) == null) { configs.put(configId, configId); } } final FileOutputStream outStream = new FileOutputStream(this.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 (final UnsupportedEncodingException ex) { outWriter = new OutputStreamWriter(outStream); encodingName = outWriter.getEncoding(); } final BufferedWriter writer = new BufferedWriter(outWriter); writer.write("<?xml version='1.0' encoding='"); writer.write(encodingName); writer.write("'?>\n"); writer.write("<history>\n"); final StringBuffer buf = new StringBuffer(200); final Enumeration<String> configEnum = configs.elements(); while (configEnum.hasMoreElements()) { final String configId = configEnum.nextElement(); buf.setLength(0); buf.append(" <processor signature=\""); buf.append(CUtil.xmlAttribEncode(configId)); buf.append("\">\n"); writer.write(buf.toString()); elements = this.history.elements(); while (elements.hasMoreElements()) { final 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()); final SourceHistory[] sourceHistories = targetHistory.getSources(); for (final SourceHistory sourceHistorie : sourceHistories) { buf.setLength(0); buf.append(" <source file=\""); buf.append(CUtil.xmlAttribEncode(sourceHistorie.getRelativePath())); buf.append("\" lastModified=\""); buf.append(Long.toHexString(sourceHistorie.getLastModified())); buf.append("\"/>\n"); writer.write(buf.toString()); } writer.write(" </target>\n"); } } writer.write(" </processor>\n"); } writer.write("</history>\n"); writer.close(); this.dirty = false; } } public TargetHistory get(final String configId, final String outputName) { TargetHistory targetHistory = this.history.get(outputName); if (targetHistory != null && !targetHistory.getProcessorConfiguration().equals(configId)) { targetHistory = null; } return targetHistory; } public File getHistoryFile() { return this.historyFile; } public void markForRebuild(final Map<String, TargetInfo> targetInfos) { for (final TargetInfo targetInfo : targetInfos.values()) { markForRebuild(targetInfo); } } // FREEHEP added synchronized public synchronized void markForRebuild(final TargetInfo targetInfo) { // // if it must already be rebuilt, no need to check further // if (!targetInfo.getRebuild()) { final TargetHistory history = get(targetInfo.getConfiguration().toString(), targetInfo.getOutput().getName()); if (history == null) { targetInfo.mustRebuild(); } else { final SourceHistory[] sourceHistories = history.getSources(); final File[] sources = targetInfo.getSources(); if (sourceHistories.length != sources.length) { targetInfo.mustRebuild(); } else { final Hashtable<String, File> sourceMap = new Hashtable<>(sources.length); for (final File source : sources) { try { sourceMap.put(source.getCanonicalPath(), source); } catch (final IOException ex) { sourceMap.put(source.getAbsolutePath(), source); } } for (final SourceHistory sourceHistorie : sourceHistories) { // // relative file name, must absolutize it on output // directory // final String absPath = sourceHistorie.getAbsolutePath(this.outputDir); File match = sourceMap.get(absPath); if (match != null) { try { match = sourceMap.get(new File(absPath).getCanonicalPath()); } catch (final IOException ex) { targetInfo.mustRebuild(); break; } } if (match == null || match.lastModified() != sourceHistorie.getLastModified()) { targetInfo.mustRebuild(); break; } } } } } } public void update(final ProcessorConfiguration config, final String[] sources, final VersionInfo versionInfo) { final String configId = config.getIdentifier(); final String[] onesource = new String[1]; String[] outputNames; for (final String source : sources) { onesource[0] = source; outputNames = config.getOutputFileNames(source, versionInfo); for (final String outputName : outputNames) { update(configId, outputName, onesource); } } } // FREEHEP added synchronized private synchronized void update(final String configId, final String outputName, final String[] sources) { final File outputFile = new File(this.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() && !CUtil.isSignificantlyBefore(outputFile.lastModified(), this.historyFile.lastModified())) { this.dirty = true; this.history.remove(outputName); final SourceHistory[] sourceHistories = new SourceHistory[sources.length]; for (int i = 0; i < sources.length; i++) { final File sourceFile = new File(sources[i]); final long lastModified = sourceFile.lastModified(); final String relativePath = CUtil.getRelativePath(this.outputDirPath, sourceFile); sourceHistories[i] = new SourceHistory(relativePath, lastModified); } final TargetHistory newHistory = new TargetHistory(configId, outputName, outputFile.lastModified(), sourceHistories); this.history.put(outputName, newHistory); } } // FREEHEP added synchronized public synchronized void update(final TargetInfo linkTarget) { final File outputFile = linkTarget.getOutput(); final 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() && !CUtil.isSignificantlyBefore(outputFile.lastModified(), this.historyFile.lastModified())) { this.dirty = true; this.history.remove(outputName); final SourceHistory[] sourceHistories = linkTarget.getSourceHistories(this.outputDirPath); final TargetHistory newHistory = new TargetHistory(linkTarget.getConfiguration().getIdentifier(), outputName, outputFile.lastModified(), sourceHistories); this.history.put(outputName, newHistory); } } }