/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2001, ThoughtWorks, Inc.
* 200 E. Randolph, 25th Floor
* Chicago, IL 60601 USA
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* + Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* + Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
********************************************************************************/
package net.sourceforge.cruisecontrol.builders;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import net.sourceforge.cruisecontrol.Builder;
import net.sourceforge.cruisecontrol.CruiseControlException;
import net.sourceforge.cruisecontrol.Progress;
import net.sourceforge.cruisecontrol.gendoc.annotations.Cardinality;
import net.sourceforge.cruisecontrol.gendoc.annotations.Default;
import net.sourceforge.cruisecontrol.gendoc.annotations.Description;
import net.sourceforge.cruisecontrol.gendoc.annotations.DescriptionFile;
import net.sourceforge.cruisecontrol.gendoc.annotations.ExamplesFile;
import net.sourceforge.cruisecontrol.gendoc.annotations.Optional;
import net.sourceforge.cruisecontrol.gendoc.annotations.Required;
import net.sourceforge.cruisecontrol.gendoc.annotations.SkipDoc;
import net.sourceforge.cruisecontrol.util.EmptyElementFilter;
import net.sourceforge.cruisecontrol.util.OSEnvironment;
import net.sourceforge.cruisecontrol.util.Util;
import net.sourceforge.cruisecontrol.util.ValidationHelper;
import net.sourceforge.cruisecontrol.util.BuildOutputLogger;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;
import org.xml.sax.SAXException;
import org.xml.sax.XMLFilter;
import org.xml.sax.helpers.XMLFilterImpl;
import org.apache.log4j.Logger;
/**
* we often see builds that fail because the previous build is still holding on to some resource.
* we can avoid this by just building in a different process which will completely die after every
* build.
*/
@DescriptionFile
@ExamplesFile
public class AntBuilder extends Builder {
protected static final String DEFAULT_LOGGER = "org.apache.tools.ant.XmlLogger";
private static final Logger LOG = Logger.getLogger(AntBuilder.class);
private String antWorkingDir;
private String buildFile = "build.xml";
private String target = "";
private String tempFileName = "log.xml";
private String antScript;
private String antHome;
private boolean useLogger;
private final List<JVMArg> args = new ArrayList<JVMArg>();
private final List<Lib> libs = new ArrayList<Lib>();
private final List<Listener> listeners = new ArrayList<Listener>();
private final List<Property> properties = new ArrayList<Property>();
private boolean useDebug = false;
private boolean useQuiet = false;
private boolean keepGoing = false;
private String loggerClassName = DEFAULT_LOGGER;
private boolean isLoggerClassNameSet;
private File saveLogDir;
private long timeout = ScriptRunner.NO_TIMEOUT;
private boolean wasValidated;
private String propertyfile;
private String progressLoggerLib;
public void validate() throws CruiseControlException {
super.validate();
ValidationHelper.assertIsSet(buildFile, "buildfile", this.getClass());
ValidationHelper.assertIsSet(target, "target", this.getClass());
ValidationHelper.assertFalse(useDebug && useQuiet,
"'useDebug' and 'useQuiet' can't be used together");
if (!useLogger && (useDebug || useQuiet)) {
LOG.warn("usedebug and usequiet are ignored if uselogger is not set to 'true'!");
}
if (saveLogDir != null) {
ValidationHelper.assertTrue(saveLogDir.isDirectory(), "'saveLogDir' must exist and be a directory");
}
ValidationHelper.assertFalse(antScript != null && antHome != null,
"'antHome' and 'antscript' cannot both be set");
// NOTE: We can't validate showProgress here because we don't know if we will really show progress until
// the AntBuilder.build() method is called (as parent Builders/Schedule may override the showProgress value).
// Validate showAntOutput
if (shouldAddDashboardLoggerJarToCommandLine(isLiveOutput(), useLogger)) {
if (progressLoggerLib == null) {
// since progressLoggerLib is not specified in the config.xml,
// we must be able to find the path to {@link AntScript#LIBNAME_PROGRESS_LOGGER}
// to ensure the separate ant VM will have access to the required listeners
AntScript.findDefaultProgressLoggerLib();
} else {
// config.xml specified progressLoggerLib, so just make sure it exists
ValidationHelper.assertExists(new File(progressLoggerLib), "progressLoggerLib", this.getClass());
}
}
if (antHome != null) {
final File antHomeFile = new File(antHome);
ValidationHelper.assertTrue(antHomeFile.exists() && antHomeFile.isDirectory(),
"'antHome' must exist and be a directory. Expected to find "
+ antHomeFile.getAbsolutePath());
final File antScriptInAntHome = new File(findAntScript(Util.isWindows()));
ValidationHelper.assertTrue(antScriptInAntHome.exists() && antScriptInAntHome.isFile(),
"'antHome' must contain an ant execution script. Expected to find "
+ antScriptInAntHome.getAbsolutePath());
antScript = antScriptInAntHome.getAbsolutePath();
}
if (antScript != null && !args.isEmpty()) {
LOG.warn("jvmargs will be ignored if you specify anthome or your own antscript!");
}
wasValidated = true;
}
/**
* build and return the results via xml. debug status can be determined
* from log4j category once we get all the logging in place.
*/
public Element build(final Map<String, String> buildProperties, final Progress progressIn)
throws CruiseControlException {
if (!wasValidated) {
throw new IllegalStateException("This builder was never validated."
+ " The build method should not be getting called.");
}
validateBuildFileExists();
final Progress progress = getShowProgress() ? progressIn : null;
final OSEnvironment antEnv = new OSEnvironment();
// Merge the environment with the configuration
mergeEnv(antEnv);
final AntScript script = new AntScript();
script.setBuildProperties(buildProperties);
script.setProperties(properties);
script.setLibs(libs);
script.setListeners(listeners);
script.setUseLogger(useLogger);
script.setUseScript(antScript != null);
script.setWindows(Util.isWindows());
script.setAntScript(antScript);
script.setArgs(args);
script.setBuildFile(buildFile);
script.setTarget(target);
script.setLoggerClassName(loggerClassName);
script.setIsLoggerClassNameSet(isLoggerClassNameSet);
script.setShowAntOutput(isLiveOutput());
script.setTempFileName(tempFileName);
script.setUseDebug(useDebug);
script.setUseQuiet(useQuiet);
script.setKeepGoing(keepGoing);
script.setSystemClassPath(getSystemClassPath());
script.setPropertyFile(propertyfile);
script.setProgressLoggerLib(progressLoggerLib);
script.setProgress(progress);
script.setAntEnv(antEnv);
final File workingDir = antWorkingDir != null ? new File(antWorkingDir) : null;
final BuildOutputLogger buildOutputConsumer;
if (isLiveOutput()) {
// TODO: I think there's a bug here when workingDir == null
buildOutputConsumer = getBuildOutputConsumer(buildProperties.get(Builder.BUILD_PROP_PROJECTNAME),
workingDir, AntOutputLogger.DEFAULT_OUTFILE_NAME);
} else {
buildOutputConsumer = null;
}
final boolean scriptCompleted = runScript(script, workingDir, buildOutputConsumer);
final File logFile = new File(antWorkingDir, tempFileName);
final Element buildLogElement;
if (!scriptCompleted) {
LOG.warn("Build timeout timer of " + timeout + " seconds has expired");
buildLogElement = new Element("build");
buildLogElement.setAttribute("error", "build timeout");
// although log file is most certainly empty, let's try to preserve it
// somebody should really fix ant's XmlLogger
if (logFile.exists()) {
try {
buildLogElement.setText(Util.readFileToString(logFile));
} catch (IOException likely) {
// ignored
}
}
} else {
//read in log file as element, return it
buildLogElement = getAntLogAsElement(logFile);
saveAntLog(logFile);
logFile.delete();
}
return buildLogElement;
}
boolean runScript(final AntScript script, final File workingDir, final BuildOutputLogger outputLogger)
throws CruiseControlException {
return new ScriptRunner().runScript(workingDir, script, timeout, outputLogger);
}
public Element buildWithTarget(final Map<String, String> properties, final String buildTarget,
final Progress progress)
throws CruiseControlException {
final String origTarget = target;
try {
target = buildTarget;
return build(properties, progress);
} finally {
target = origTarget;
}
}
void validateBuildFileExists() throws CruiseControlException {
File build = new File(buildFile);
if (!build.exists() && !build.isAbsolute() && antWorkingDir != null) {
build = new File(antWorkingDir, buildFile);
}
ValidationHelper.assertExists(build, "buildfile", this.getClass());
}
@Description(
"If supplied, a copy of the ant log will be saved in the specified "
+ "local directory. Example: saveLogDir=\"/usr/local/dev/projects/cc/logs\".")
@Optional
public void setSaveLogDir(String dir) {
saveLogDir = null;
if (dir != null && !dir.trim().equals("")) {
saveLogDir = new File(dir.trim());
}
}
void saveAntLog(File logFile) {
if (saveLogDir == null) {
return;
}
try {
final File newAntLogFile = new File(saveLogDir, tempFileName);
newAntLogFile.createNewFile();
final FileInputStream in = new FileInputStream(logFile);
try {
final FileOutputStream out = new FileOutputStream(newAntLogFile);
try {
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
} finally {
out.close();
}
} finally {
in.close();
}
} catch (IOException ioe) {
LOG.error(ioe);
LOG.error("Unable to create file: " + new File(saveLogDir, tempFileName));
}
}
@Description(
"Will invoke ANT in the specified directory. This directory can be "
+ "absolute or relative to the cruisecontrol working directory.")
@Optional
public void setAntWorkingDir(String dir) {
antWorkingDir = dir;
}
@Description(
"Absolute filename of script (shell script or bat file) used to start Ant. "
+ "You can use this to make CruiseControl use your own Ant installation. "
+ "If this is not specified, the AntBuilder uses the Ant distribution that "
+ "ships with CruiseControl. See below for <a href=\"#ant-examples\">examples"
+ "</a>.")
@Optional(
"Recommended, however. Cannot be specified if anthome attribute "
+ "is also specified")
public void setAntScript(String antScript) {
this.antScript = antScript;
}
@Description(
"Directory in which Ant is installed. CruiseControl will attempt to use the "
+ "standard Ant execution scripts (i.e. ant.bat or ant). See below for "
+ "<a href=\"#ant-examples\">examples</a>.")
@Optional("Cannot be specified if antscript attribute is also specified.")
public void setAntHome(String antHome) {
this.antHome = antHome;
}
/**
* @param isWindows if true, running under windows
* @return If the anthome attribute is set, then this method returns the correct shell script
* to use for a specific environment.
* @throws CruiseControlException if <code>antHome</code> is not set
*/
protected String findAntScript(boolean isWindows) throws CruiseControlException {
if (antHome == null) {
throw new CruiseControlException("anthome attribute not set.");
}
if (isWindows) {
return antHome + "\\bin\\ant.bat";
} else {
return antHome + "/bin/ant";
}
}
@Description("Name of temp file used to capture output.")
@Optional
@Default("log.xml")
public void setTempFile(String tempFileName) {
this.tempFileName = tempFileName;
}
@Description(
"Ant target(s) to run. Default is \"\", or the default target for "
+ "the build file.")
@Optional
public void setTarget(String target) {
this.target = target;
}
@Description("Path to Ant build file.")
@Optional
@Default("build.xml")
public void setBuildFile(String buildFile) {
this.buildFile = buildFile;
}
@Description(
"'true' if CruiseControl should call Ant using -logger; 'false' to call Ant "
+ "using '-listener', thus using the loggerclass as a Listener. uselogger="
+ "\"true\" will make Ant log its messages using the class specified by "
+ "loggerclassname as an Ant Logger, which can make for smaller log files since "
+ "it doesn't log DEBUG messages (see useDebug and useQuiet attributes below, "
+ "and the <a href=\"http://ant.apache.org/manual/listeners.html\">Ant manual</a>). "
+ "Set to false to have Ant echo ant messages to console "
+ "using its DefaultLogger, which is useful when debugging your ant build. "
+ "Defaults to 'false' to make initial setup easier but setting it to 'true' is "
+ "recommended for production situations."
+ "<br/><br/>"
+ "RE: liveOutput: If liveOutput=true AND uselogger=true, this builder will write "
+ "the ant output to a file (antBuilderOutput.log) that can be read by the "
+ "Dashboard reporting application. The liveOutput setting has no effect if "
+ "uselogger=false. <a href=\"#antbootstrapper\">AntBootstrapper</a> and "
+ "<a href=\"#antpublisher\">AntPublisher</a> do not provide access to "
+ "liveOutput, and operate as if liveOutput=false. NOTE: In order to show ant "
+ "output while uselogger=true, the AntBuilder uses a custom Build Listener. If "
+ "this interferes with your Ant build, set liveOutput=false (and please report "
+ "the problem)")
@Optional
public void setUseLogger(boolean useLogger) {
this.useLogger = useLogger;
}
/**
* Sets whether Ant will use the custom AntOutputLogger as a listener in order to show live output.
* @param showAntOutput if true, add AntOutputLogger as a listener.
* @deprecated Use {@link #setLiveOutput(boolean)} instead.
*/
@SkipDoc
public void setShowAntOutput(final boolean showAntOutput) {
setLiveOutput(showAntOutput);
}
/**
* @return true if Ant will use the custom AntOutputLogger as a listener in order to show live output.
* @deprecated Use {@link #isLiveOutput()} instead.
*/
boolean getShowAntOutput() {
return isLiveOutput();
}
/**
* @param showAntOutput if false, disables Dashboard AntOutputLogger
* @param useLogger if false, disables Dashboard AntOutputLogger
* @return true if the jar containing the custom Dashboard logger class must be added to the command line used
* to execute Ant.
*/
static boolean shouldAddDashboardLoggerJarToCommandLine(final boolean showAntOutput, final boolean useLogger) {
return showAntOutput && useLogger;
}
@Description("Pass specified argument to the jvm used to invoke ant."
+ "Ignored if using anthome or antscript. The element has a single required"
+ "attribute: \"arg\".<br />"
+ "<strong>Example:</strong> <code><jvmarg arg=\"-Xmx120m\"/></code>")
@Cardinality(min = 0, max = -1)
public JVMArg createJVMArg() {
final JVMArg arg = new JVMArg();
args.add(arg);
return arg;
}
@Description("Used to define additional <a "
+ "href=\"http://ant.apache.org/manual/running.html#libs\">library directories</a> "
+ "for the ant build. The element has one required attribute: \"searchPath\".<br /> "
+ "<strong>Example:</strong> <code><lib searchPath=\"/home/me/myantextensions\"/"
+ "></code>")
@Cardinality(min = 0, max = -1)
public Lib createLib() {
final Lib lib = new Lib();
libs.add(lib);
return lib;
}
@Description("Used to define additional <a "
+ "href=\"http://ant.apache.org/manual/listeners.html\">listeners</a> for the "
+ "ant build. The element has one required attribute: \"classname\".<br />"
+ "<strong>Example:</strong> <code><listener classname=\"org.apache.tools."
+ "ant.listener.Log4jListener\"/></code>")
@Cardinality(min = 0, max = -1)
public Listener createListener() {
final Listener listener = new Listener();
listeners.add(listener);
return listener;
}
@Description("Used to define properties for the ant build. The element has two "
+ "required attributes: \"name\" and \"value\". These will be passed on the "
+ "ant command-line as \"-Dname=value\"<br />"
+ "<strong>Example:</strong> <code><property name=\"foo\" value=\"bar\"/"
+ "></code>")
@Cardinality(min = 0, max = -1)
public Property createProperty() {
final Property property = new Property();
properties.add(property);
return property;
}
protected String getSystemClassPath() {
return System.getProperty("java.class.path");
}
protected Element getAntLogAsElement(File file) throws CruiseControlException {
if (!file.exists()) {
throw new CruiseControlException("ant logfile " + file.getAbsolutePath() + " does not exist.");
} else if (file.length() == 0) {
throw new CruiseControlException("ant logfile " + file.getAbsolutePath()
+ " is empty. Your build probably failed. Check your CruiseControl logs.");
}
try {
SAXBuilder builder = new SAXBuilder("org.apache.xerces.parsers.SAXParser");
// old Ant-versions contain a bug in the XmlLogger that outputs
// an invalid PI containing the target "xml:stylesheet"
// instead of "xml-stylesheet": fix this
XMLFilter piFilter = new XMLFilterImpl() {
public void processingInstruction(String target, String data) throws SAXException {
if (target.equals("xml:stylesheet")) { target = "xml-stylesheet"; }
super.processingInstruction(target, data);
}
};
// get rid of empty <task>- and <message>-elements created by Ant's XmlLogger
XMLFilter emptyTaskFilter = new EmptyElementFilter("task");
emptyTaskFilter.setParent(piFilter);
XMLFilter emptyMessageFilter = new EmptyElementFilter("message");
emptyMessageFilter.setParent(emptyTaskFilter);
builder.setXMLFilter(emptyMessageFilter);
return builder.build(file).getRootElement();
} catch (Exception ee) {
if (ee instanceof CruiseControlException) {
throw (CruiseControlException) ee;
}
File saveFile = new File(file.getParentFile(), System.currentTimeMillis() + file.getName());
file.renameTo(saveFile);
throw new CruiseControlException("Error reading : " + file.getAbsolutePath()
+ ". Saved as : " + saveFile.getAbsolutePath(), ee);
}
}
@Description(
"If true will invoke ant with -debug, which can be useful for debugging your "
+ "ant build. Defaults to 'false', cannot be set to 'true' if usequiet is "
+ "also set to 'true'. When used in combination with uselogger=\"true\", "
+ "this will result in bigger XML log files; otherwise, it will cause more "
+ "output to be written to the console by Ant's DefaultLogger.")
@Optional
public void setUseDebug(boolean debug) {
useDebug = debug;
}
@Description(
"If true will invoke ant with -quiet, which can be useful for creating smaller "
+ "log files since messages with a priority of INFO will not be logged. Defaults "
+ "to 'false', cannot be set to 'true' if usedebug is also set to 'true'. "
+ "Smaller logfiles are only achieved when used in combination with uselogger="
+ "\"true\", otherwise there will just be less output echoed to the console by "
+ "Ant's DefaultLogger."
+ "<br/><br/>"
+ "RE: showProgress: useQuiet=\"true\" will prevent any progress messages from "
+ "being displayed. NOTE: In order to show progress, the AntBuilder uses custom "
+ "Build Loggers and Listeners. If these interfere with your Ant build, set "
+ "showProgress=false (and please report the problem).")
@Optional
public void setUseQuiet(boolean quiet) {
useQuiet = quiet;
}
@Description(
"If true will invoke ant with -keep-going, which can be useful for performing "
+ "build steps after an optional step fails. Defaults to 'false'.")
@Optional
public void setKeepGoing(boolean keepGoing) {
this.keepGoing = keepGoing;
}
public String getLoggerClassName() {
return loggerClassName;
}
@Description(
"If you want to use another logger (or listener, when uselogger=\"false\") than "
+ "Ant's XmlLogger, you can specify the classname of the logger here. The logger "
+ "needs to output compatible XML, and the class needs to be available on the "
+ "classpath at buildtime.")
@Optional
@Default("org.apache.tools.ant.XmlLogger")
public void setLoggerClassName(String string) {
loggerClassName = string;
isLoggerClassNameSet = true;
}
@Description("Passes an argument to the JVM used to invoke ANT.")
public class JVMArg implements Serializable {
private static final long serialVersionUID = 402625457108399047L;
private String arg;
@Description("Command-line argument to pass to the ANT JVM.")
@Required
public void setArg(String arg) {
this.arg = arg;
}
public String getArg() {
return arg;
}
}
@Description("Provides additional library directories for an ANT build.")
public class Lib implements Serializable {
private static final long serialVersionUID = 1804469347425625224L;
private String searchPath;
@Description("Path to use for loading libraries into the ANT JVM.")
@Required
public void setSearchPath(String searchPath) {
this.searchPath = searchPath;
}
public String getSearchPath() {
return searchPath;
}
}
@Description("Provides additional listeners for an ANT build.")
public class Listener implements Serializable {
private static final long serialVersionUID = 4813682685614734386L;
private String className;
@Description("Name of the Java listener class to register with this ANT build.")
@Required
public void setClassName(String className) {
this.className = className;
}
public String getClassName() {
return className;
}
}
@Description(
"Ant build will be halted if it continues longer than the specified timeout. "
+ "Value in seconds.")
@Optional
public void setTimeout(long timeout) {
this.timeout = timeout;
}
@Description(
"Load all properties from file with -D properties (like child <code><a href=\""
+ "#antbuilderchildprop\"><property></a></code> elements) taking "
+ "precedence. Useful when the propertyfile content can change for every build.")
@Optional
public void setPropertyfile(String propertyfile) {
this.propertyfile = propertyfile;
}
@Description(
"Overrides the default -lib search path used to add support for showProgress "
+ "features in the ant builder. This search path ensures customized ant "
+ "Loggers/Listeners are available on the classpath of the ant builder VM. You "
+ "should not normally set this value. If you do set this value, you should "
+ "use the full path (including filename) to cruisecontrol-antprogresslogger.jar. "
+ "This setting has no effect if showProgress=false.")
@Optional
public void setProgressLoggerLib(String progressLoggerLib) {
this.progressLoggerLib = progressLoggerLib;
}
/**
* @return The path (including filename) to the jar file
* ({@link AntScript#LIBNAME_PROGRESS_LOGGER cruisecontrol-antprogresslogger.jar})
* containing the AntProgressLogger/Listener classes.
*/
public String getProgressLoggerLib() {
return progressLoggerLib;
}
}