Index: cruisecontrol/main/docs/configxml.html =================================================================== --- cruisecontrol/main/docs/configxml.html (revision 2899) +++ cruisecontrol/main/docs/configxml.html (working copy) @@ -149,6 +149,7 @@ <mks> <modificationset> <nant> +<phing> <onfailure> <onsuccess> <p4> @@ -285,6 +286,7 @@ <maven/> <pause/> <nant/> + <phing/> <exec/> <composite/> </schedule> @@ -4476,6 +4478,11 @@ 0 .. * Builds the project using NAnt. + + <phing> + 0 .. * + Builds the project using Phing. + @@ -5106,6 +5113,11 @@ 0 .. * Builds the project using NAnt. + + <phing> + 0 .. * + Builds the project using Phing. + <exec> 0 .. * @@ -5214,7 +5226,7 @@ No (defaults to default.build) Path to NAnt build file. - + < target No NAnt target(s) to run. Default is "", or the default target @@ -5357,6 +5369,203 @@ + +
+ top +

<phing>

+ +
+
+<cruisecontrol>
+  <project>
+    <schedule>
+      <phing>
+
+ +

Specifies a Phing build to be run at a given time or build number. + For instance, we can run a clean build every 5 builds, or run more + comprehensive (and time intensive) system tests every 10 builds. We + can also schedule an official build to run every night at midnight, + if we so desire.

+ +

When a build runs, Phing is invoked in a separate Java process. Phing and the PHP requisites need + to be installed for this builder to function. See Phing's documentation for details on installation + and configuration.

+ +

The standard CruiseControl properties + passed to builders are available from within the Phing build.

+ +

See below for examples of the + <phing> element.

+ +

Attributes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeRequiredDescription
buildfileNo (defaults to build.xml)Path to Phing build file.
targetNoPhing target(s) to run. Default is "", or the default target + for the build file.
multipleNoBuild index used to run different builders. For example, + if this is set to 3, the builder will be run every 3 builds. + Default value is 1. Can't be set if time is set.
timeNoTime in the form HHmm. Can't be set if multiple is set.
dayNoValid values are (case-insensitive) the English names for + the days of the week (Sunday, Monday, Tuesday, etc). Does not + support multiple days except for the default of every day.
tempfileNoName of temp file. Defaults to log.xml
phingscriptNo. Cannot be specified if phinghome attribute is also specifiedAbsolute filename of script (shell script or bat file) used + to start Phing. If this is not specified, the PhingBuilder assumes that phing (or phing.bat) + is installed on your path.
phinghomeNo. Cannot be specified if phingscript attribute is also specified.Directory in which Phing is installed. CruiseControl will attempt to use the standard + Phing execution scripts (i.e. phing.bat or phing). See below for examples.
phingWorkingDirNoWill invoke phing in the specified directory.
saveLogDirNoIf supplied a copy of the phing log will be saved in the specified directory. Example: +saveLogDir="/usr/local/dev/projects/cc/logs"
timeoutNoPhing build will be halted if it continues longer than the + specified timeout. Value in seconds.
useloggerNo'true' if CruiseControl should call Phing using -logger; 'false' to call Phing using '-listener', + thus using the loggerclass as a Listener. uselogger="true" will make Phing log its messages using the + class specified by loggerclassname as a Phing Logger, which can make for smaller log files since it + doesn't log DEBUG messages (see useDebug and useQuiet attributes below, and the Phing manual). Set to false + to have Phing echo messages to console using its DefaultLogger, which is useful when debugging your Phing + build. Defaults to 'false' to make initial setup easier but setting it to 'true' is recommended for + production situations.
loggerclassnameNo (defaults to phing.listener.XmlLogger)If you want to use another logger (or listener, when uselogger="false") than Phing'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.
usedebugNoIf true will invoke phing with -debug, which can be + useful for debugging your phing 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 Phing's + DefaultLogger.
usequietNoIf true will invoke phing 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 Phing's DefaultLogger.
+ +

Child Elements

+ + + + + + + + + + + + + + + + +
ElementCardinalityDescription
<property>0 .. *Used to define properties for the phing build. The element + has two required attributes: "name" and "value". These will + be passed on the phing command-line as "-Dname=value"
+ Example: <property name="foo" value="bar"/>
+ +

Examples

+ +
    +
  1. Invoke the phing.bat script distributed with phing using the phingscript attribute, + specifying the working directory as D:\workspace\MyProject and the + phing build file as MyProject-nightlybuild.xml using the default target. + +
    <schedule>
    +    <phing phingscript="C:\PHP\applications\phing-2.3.0\bin\phing.bat"
    +         phingworkingdir="D:\workspace\MyProject"
    +         buildfile="MyProject-nightlybuild.xml"
    +         uselogger="true"
    +         usedebug="false"/>
    +<schedule>
    + + Or equivalently, using the phinghome attribute + +
    <schedule>
    +    <phing phinghome="C:\PHP\applications\phing-2.3.0\bin\"
    +         phingworkingdir="D:\workspace\MyProject"
    +         buildfile="MyProject-nightlybuild.xml"
    +         uselogger="true"
    +         usedebug="false"/>
    +<schedule>
    +
  2. +
+
top Index: cruisecontrol/main/src/net/sourceforge/cruisecontrol/builders/PhingBuilder.java =================================================================== --- cruisecontrol/main/src/net/sourceforge/cruisecontrol/builders/PhingBuilder.java (revision 0) +++ cruisecontrol/main/src/net/sourceforge/cruisecontrol/builders/PhingBuilder.java (revision 0) @@ -0,0 +1,343 @@ +package net.sourceforge.cruisecontrol.builders; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +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.util.EmptyElementFilter; +import net.sourceforge.cruisecontrol.util.Util; +import net.sourceforge.cruisecontrol.util.ValidationHelper; + +import org.apache.log4j.Logger; +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; + +public class PhingBuilder extends Builder { + + protected static final String DEFAULT_LOGGER = "phing.listener.XmlLogger"; + private static final Logger LOG = Logger.getLogger(AntBuilder.class); + + private String phingWorkingDir = null; + private String buildFile = "build.xml"; + private String target = ""; + private String tempFileName = "log.xml"; + private String phingScript = "phing"; + private String phingHome; + private boolean useLogger; + private List properties = new ArrayList(); + private boolean useDebug = false; + private boolean useQuiet = false; + private String loggerClassName = DEFAULT_LOGGER; + private File saveLogDir = null; + private long timeout = ScriptRunner.NO_TIMEOUT; + private boolean wasValidated = false; + + 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"); + } + + if (phingHome != null) { + final File phingHomeFile = new File(phingHome); + ValidationHelper.assertTrue(phingHomeFile.exists() && phingHomeFile.isDirectory(), + "'phingHome' must exist and be a directory. Expected to find " + + phingHomeFile.getAbsolutePath()); + + final File phingScriptInPhingHome = new File(findPhingScript(Util.isWindows())); + ValidationHelper.assertTrue(phingScriptInPhingHome.exists() && phingScriptInPhingHome.isFile(), + "'phingHome' must contain an ant execution script. Expected to find " + + phingScriptInPhingHome.getAbsolutePath()); + + phingScript = phingScriptInPhingHome.getAbsolutePath(); + } + + 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(Map buildProperties) throws CruiseControlException { + if (!wasValidated) { + throw new IllegalStateException("This builder was never validated." + + " The build method should not be getting called."); + } + + validateBuildFileExists(); + + PhingScript script = new PhingScript(); + script.setBuildProperties(buildProperties); + script.setProperties(properties); + script.setUseLogger(useLogger); + script.setWindows(Util.isWindows()); + script.setPhingScript(phingScript); + script.setBuildFile(buildFile); + script.setTarget(target); + script.setLoggerClassName(loggerClassName); + script.setTempFileName(tempFileName); + script.setUseDebug(useDebug); + script.setUseQuiet(useQuiet); + + File workingDir = phingWorkingDir != null ? new File(phingWorkingDir) : null; + + boolean scriptCompleted = new ScriptRunner().runScript(workingDir, script, timeout); + + File logFile = new File(phingWorkingDir, tempFileName); + 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) { + } + } + } else { + //read in log file as element, return it + buildLogElement = getPhingLogAsElement(logFile); + savePhingLog(logFile); + logFile.delete(); + } + return buildLogElement; + } + + public Element buildWithTarget(Map properties, String buildTarget) throws CruiseControlException { + String origTarget = target; + try { + target = buildTarget; + return build(properties); + } finally { + target = origTarget; + } + } + + void validateBuildFileExists() throws CruiseControlException { + File build = new File(buildFile); + if (!build.isAbsolute() && phingWorkingDir != null) { + build = new File(phingWorkingDir, buildFile); + } + ValidationHelper.assertExists(build, "buildfile", this.getClass()); + } + + + /** + * Set the location to which the ant log will be saved before Cruise + * Control merges the file into its log. + * + * @param dir + * the absolute path to the directory where the ant log will be + * saved or relative path to where you started CruiseControl + */ + public void setSaveLogDir(String dir) { + saveLogDir = null; + + if (dir != null && !dir.trim().equals("")) { + saveLogDir = new File(dir.trim()); + } + } + + void savePhingLog(File logFile) { + if (saveLogDir == null) { + return; + } + + try { + File newPhingLogFile = new File(saveLogDir, tempFileName); + newPhingLogFile.createNewFile(); + + FileInputStream in = new FileInputStream(logFile); + FileOutputStream out = new FileOutputStream(newPhingLogFile); + + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + in.close(); + out.close(); + } catch (IOException ioe) { + LOG.error(ioe); + LOG.error("Unable to create file: " + new File(saveLogDir, tempFileName)); + } + } + + /** + * Set the working directory where Phing will be invoked. This parameter gets + * set in the XML file via the phingWorkingDir attribute. The directory can + * be relative (to the cruisecontrol current working directory) or absolute. + * + * @param dir + * the directory to make the current working directory. + */ + public void setPhingWorkingDir(String dir) { + phingWorkingDir = dir; + } + + /** + * Sets the Phing script file to be invoked. + * + * This is a platform dependent script file. + * + * @param phingScript the name of the script file + */ + public void setPhingScript(String phingScript) { + this.phingScript = phingScript; + } + + /** + * If set CC will use the platform specific script provided by Phing + * + * @param antHome the path to ANT_HOME + */ + public void setPhingHome(String antHome) { + this.phingHome = antHome; + } + + /** + * If the phinghome attribute is set, then this method returns the correct shell script + * to use for a specific environment. + */ + protected String findPhingScript(boolean isWindows) throws CruiseControlException { + if (phingHome == null) { + throw new CruiseControlException("phinghome attribute not set."); + } + + if (isWindows) { + return phingHome + "\\bin\\phing.bat"; + } else { + return phingHome + "/bin/phing"; + } + } + + /** + * Set the name of the temporary file used to capture output. + * + * @param tempFileName + */ + public void setTempFile(String tempFileName) { + this.tempFileName = tempFileName; + } + + /** + * Set the Phing target(s) to invoke. + * + * @param target the target(s) name. + */ + public void setTarget(String target) { + this.target = target; + } + + /** + * Sets the name of the build file that Phing will use. The Phing default is + * build.xml, use this to override it. + * + * @param buildFile the name of the build file. + */ + public void setBuildFile(String buildFile) { + this.buildFile = buildFile; + } + + /** + * Sets whether Phing will use the custom loggers. + * + * @param useLogger + */ + public void setUseLogger(boolean useLogger) { + this.useLogger = useLogger; + } + + public Property createProperty() { + Property property = new Property(); + properties.add(property); + return property; + } + + protected static Element getPhingLogAsElement(File file) throws CruiseControlException { + if (!file.exists()) { + throw new CruiseControlException("phing logfile " + file.getAbsolutePath() + " does not exist."); + } else if (file.length() == 0) { + throw new CruiseControlException("phing 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 + // FIXME - remove this, as it shouldn't affect Phing (though I don't know what this is + // for yet.) + 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 - and -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); + } + } + + public void setUseDebug(boolean debug) { + useDebug = debug; + } + + public void setUseQuiet(boolean quiet) { + useQuiet = quiet; + } + + public String getLoggerClassName() { + return loggerClassName; + } + + public void setLoggerClassName(String string) { + loggerClassName = string; + } + + /** + * @param timeout The timeout to set. + */ + public void setTimeout(long timeout) { + this.timeout = timeout; + } +} Index: cruisecontrol/main/src/net/sourceforge/cruisecontrol/builders/PhingScript.java =================================================================== --- cruisecontrol/main/src/net/sourceforge/cruisecontrol/builders/PhingScript.java (revision 0) +++ cruisecontrol/main/src/net/sourceforge/cruisecontrol/builders/PhingScript.java (revision 0) @@ -0,0 +1,167 @@ +package net.sourceforge.cruisecontrol.builders; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import net.sourceforge.cruisecontrol.CruiseControlException; +import net.sourceforge.cruisecontrol.util.Commandline; + +public class PhingScript implements Script { + private Map buildProperties; + + private boolean isWindows; + private String phingScript; + private String loggerClassName; + private String tempFileName = "log.xml"; + private boolean useLogger; + private boolean useQuiet; + private boolean useDebug; + private String buildFile = "build.xml"; + private List properties; + private String target = ""; + private int exitCode; + + + /** + * construct the command that we're going to execute. + * + * @return Commandline holding command to be executed + * @throws CruiseControlException on unquotable attributes + */ + public Commandline buildCommandline() throws CruiseControlException { + + Commandline cmdLine = new Commandline(); + + cmdLine.setExecutable(phingScript); + + if (useLogger) { + cmdLine.createArguments("-logger", getLoggerClassName()); + cmdLine.createArguments("-logfile", tempFileName); + } else { + cmdLine.createArguments("-listener", getLoggerClassName()); + cmdLine.createArgument("-DXmlLogger.file=" + tempFileName); + } + + // -debug and -quiet only affect loggers, not listeners: when we use the loggerClassName as + // a listener, they will affect the default logger that writes to the console + if (useDebug) { + cmdLine.createArgument("-debug"); + } else if (useQuiet) { + cmdLine.createArgument("-quiet"); + } + + for (Iterator propertiesIter = buildProperties.entrySet().iterator(); propertiesIter.hasNext(); ) { + Map.Entry property = (Map.Entry) propertiesIter.next(); + String value = (String) property.getValue(); + if (!"".equals(value)) { + cmdLine.createArgument("-D" + property.getKey() + "=" + value); + } + } + + for (Iterator antPropertiesIterator = properties.iterator(); antPropertiesIterator.hasNext(); ) { + Property property = (Property) antPropertiesIterator.next(); + cmdLine.createArgument("-D" + property.getName() + "=" + property.getValue()); + } + + cmdLine.createArguments("-buildfile", buildFile); + + StringTokenizer targets = new StringTokenizer(target); + while (targets.hasMoreTokens()) { + cmdLine.createArgument(targets.nextToken()); + } + + System.out.println(cmdLine.toString()); + + return cmdLine; + } + + /** + * @param buildProperties The buildProperties to set. + */ + public void setBuildProperties(Map buildProperties) { + this.buildProperties = buildProperties; + } + + /** + * @return Returns the loggerClassName. + */ + public String getLoggerClassName() { + return loggerClassName; + } + /** + * @param loggerClassName The loggerClassName to set. + */ + public void setLoggerClassName(String loggerClassName) { + this.loggerClassName = loggerClassName; + } + /** + * @param antScript The antScript to set. + */ + public void setPhingScript(String antScript) { + this.phingScript = antScript; + } + + /** + * @param isWindows The isWindows to set. + */ + public void setWindows(boolean isWindows) { + this.isWindows = isWindows; + } + /** + * @param buildFile The buildFile to set. + */ + public void setBuildFile(String buildFile) { + this.buildFile = buildFile; + } + /** + * @param tempFileName The tempFileName to set. + */ + public void setTempFileName(String tempFileName) { + this.tempFileName = tempFileName; + } + /** + * @param useDebug The useDebug to set. + */ + public void setUseDebug(boolean useDebug) { + this.useDebug = useDebug; + } + /** + * @param useLogger The useLogger to set. + */ + public void setUseLogger(boolean useLogger) { + this.useLogger = useLogger; + } + /** + * @param useQuiet The useQuiet to set. + */ + public void setUseQuiet(boolean useQuiet) { + this.useQuiet = useQuiet; + } + + /** + * @param properties The properties to set. + */ + public void setProperties(List properties) { + this.properties = properties; + } + /** + * @param target The target to set. + */ + public void setTarget(String target) { + this.target = target; + } + /** + * @return Returns the exitCode. + */ + public int getExitCode() { + return exitCode; + } + /** + * @param exitCode The exitCode to set. + */ + public void setExitCode(int exitCode) { + this.exitCode = exitCode; + } +} Index: cruisecontrol/main/src/net/sourceforge/cruisecontrol/default-plugins.properties =================================================================== --- cruisecontrol/main/src/net/sourceforge/cruisecontrol/default-plugins.properties (revision 2899) +++ cruisecontrol/main/src/net/sourceforge/cruisecontrol/default-plugins.properties (working copy) @@ -57,6 +57,7 @@ maven2=net.sourceforge.cruisecontrol.builders.Maven2Builder nant=net.sourceforge.cruisecontrol.builders.NantBuilder pause=net.sourceforge.cruisecontrol.PauseBuilder +phing=net.sourceforge.cruisecontrol.PhingBuilder rake=net.sourceforge.cruisecontrol.builders.RakeBuilder # label incrementer -- only one!