/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2003, 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.StringWriter;
import java.util.LinkedList;
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.Default;
import net.sourceforge.cruisecontrol.gendoc.annotations.ManualChildName;
import net.sourceforge.cruisecontrol.gendoc.annotations.Required;
import net.sourceforge.cruisecontrol.gendoc.annotations.SkipDoc;
import net.sourceforge.cruisecontrol.util.DateUtil;
import net.sourceforge.cruisecontrol.util.IO;
import net.sourceforge.cruisecontrol.util.OSEnvironment;
import net.sourceforge.cruisecontrol.util.ValidationHelper;
import org.apache.log4j.Logger;
import org.jdom.Attribute;
import org.jdom.Element;
/**
* Builder class for CMake C/C++ build system.
*
* Executes <tt>cmake</tt> command with a given parameters, followed by the user-defined sequence
* of commands. Builder fails if any of the commands fail.
*
* Although there is possibility to use {@link CompositeBuilder}, this class has the
* following advantages:
* <ul>
* <li> more user-friendly format than when using raw {@link ExecBuilder}:
* <pre>
* {@code
* <exec command="rm" args="-f /path/to/build/dir"/>
* <exec command="mkdir" args="/path/to/build/dir"/>
* <exec command="cmake"
* args="-D CMAKE_VERBOSE_MAKEFILE:BOOL=ON -D.... -D INSTALL_PREFIX=/usr/local/bin
* -U USER_UNDEF, -U ... -G 'Unix Makefiles' /path/to/source/root"
* workingdir="/path/to/build/dir"/>
* <exec command="make"
* workingdir="/path/to/build/dir"/>
* <exec command="make" args="install"
* workingdir="/path/to/build/dir"/>
* </pre>
* will look with CMake builder as follows:
* <pre>
* <cmake srcroot="/path/to/source/root"
* builddir="/path/to/build/dir" cleanbuild="true">
*
* <option value="-D CMAKE_VERBOSE_MAKEFILE:BOOL=ON"/>
* ...
* <option value="-D INSTALL_PREFIX=/usr/local/bin"/>
* <option value="-U USER_UNDEF"/>
* ...
* <option value="-G 'Unix Makefiles'"/>
*
* <build exec="make" />
* <build exec="make" args="install"/>
* </cmake>
* }
* </pre>
* <li> CMake can be pre-configured using {@code <plugin />} framework
* <li> the build directory is automatically created if not existing.
* <li> each build can start in empty build directory, see {@link #setCleanBuild(boolean)}.
* <li> the output of <tt>make test</tt> can be integrated to the output of CC as it is in case of
* ANT builder (<b>not finished yet!</b>)
* </ul>
*
* @author <a href="mailto:dtihelka@kky.zcu.cz">Dan Tihelka</a>
*/
public class CMakeBuilder extends Builder {
/**
* Validate the attributes for the plugin.
*/
@Override
public void validate() throws CruiseControlException {
super.validate();
final File builddir = getBuildDir();
final File srcroot = getSrcRoot();
/* A top-level CMake file and build directory arguments are required to be defined */
ValidationHelper.assertIsSet(builddir, "builddir", this.getClass());
ValidationHelper.assertIsSet(srcroot, "srcroot", this.getClass());
/* Check the existence of source directory and CMakeLists.txt file in it. */
ValidationHelper.assertExists(srcroot, "srcroot", this.getClass());
ValidationHelper.assertExists(new File(srcroot, "CMakeLists.txt"), "srcroot", this.getClass());
/* There must not be a file with the name set in buildDir */
ValidationHelper.assertFalse(builddir.isFile(), "There is file '" + builddir
+ "existing, but it is required to be build directory");
/* Validate all the cmake defines */
for (final Option option : getOptions()) {
ValidationHelper.assertNotEmpty(option.toString(), "option", option.getClass());
}
/* Build the commands to execute. The first is "raw" cmake */
final ExecBuilderCMake builder = createBuilder();
builder.setCommand("cmake");
/* Options for CMake */
for (final Option option : getOptions()) {
builder.addOption(option);
}
/* srcRoot - path to CMakeLists.txt */
builder.addPath(srcroot);
/* CMake is the very first */
commands.addFirst(builder);
/* Set inherited properties and validate individual commands */
for (final ExecBuilder c : getBuilders()) {
if (c.getWorkingDir() == null) {
c.setWorkingDir(builddir.getAbsolutePath());
}
c.setLiveOutput(isLiveOutput());
c.setMultiple(getMultiple());
c.setShowProgress(getShowProgress());
// TODO setDate(), setTime()??
/* Validate */
c.validate();
}
}
/**
* Executes the commands and return the results as XML
*/
@Override
public Element build(final Map<String, String> buildProperties, final Progress progressIn)
throws CruiseControlException {
long startTime = System.currentTimeMillis();
Element buildLogElement = new Element("build");
Element cmmndLogElement;
Attribute cmndErrAttrib;
File builddir = getBuildDir();
/* If clean directory is required, clean it */
if (getCleanBuild()) {
IO.delete(builddir);
}
/* Creates the build directory, if it does not exist. */
if (!builddir.exists()) {
builddir.mkdirs();
}
/* Call all the commands one after another */
for (final ExecBuilder c : getBuilders()) {
final long timeout = getTimeout();
final long remainTime = timeout != ScriptRunner.NO_TIMEOUT
? timeout - (System.currentTimeMillis() - startTime) / 1000
: Long.MAX_VALUE;
/* timeout - it must be set dynamically ...*/
if (c.getTimeout() == ScriptRunner.NO_TIMEOUT || c.getTimeout() > remainTime) {
c.setTimeout(remainTime);
}
/* start the command and store its output into the overall logElement */
cmmndLogElement = c.build(buildProperties, progressIn);
buildLogElement.addContent(cmmndLogElement);
/* if the result contains "error" attribute, I suppose it failed. Copy the error to the top-level
to signalize CC that something went wrong, and the build failed */
cmndErrAttrib = cmmndLogElement.getAttribute("error");
if (cmndErrAttrib != null) {
buildLogElement.setAttribute(cmndErrAttrib.detach());
break;
}
}
/* Set the time it took to exec the whole command. Taken from ExecBuilder#build() */
buildLogElement.setAttribute("time", DateUtil.getDurationAsString((System.currentTimeMillis() - startTime)));
/* Return the whole build log */
return buildLogElement;
}
/**
* ????
*/
@Override
public Element buildWithTarget(final Map<String, String> properties, final String target,
final Progress progress) throws CruiseControlException {
// TODO: finish it
throw new CruiseControlException("Method not implemented! I do not understand its difference from #build()");
}
/**
* Sets the directory where the top-level <tt>CMakeLists.txt</tt> is located. The attribute
* is required.
*
* @param path the path to the top-level <tt>CMakeLists.txt</tt> file.
*/
@Required
public void setSrcRoot(String path) {
this.srcRoot = new File(path);
}
/**
* @return path set through {@link #setSrcRoot(String)} or <code>NULL</code> if the path has not been set.
*/
public File getSrcRoot() {
return this.srcRoot;
}
/**
* Sets the build directory into which <tt>cmake</tt> creates <tt>Makefile</tt>, and into which the
* project is built. The attribute is required.
*
* @param path the path to the build directory.
*/
@Required
public void setBuildDir(String path) {
this.buildDir = new File(path);
}
/**
* @return path set through {@link #setBuildDir(String)} or <code>NULL</code> if the path has not been set.
*/
public File getBuildDir() {
return this.buildDir;
}
/**
* Should the build be started from the clean location? If set to <code>true</code>, the content
* of directory set in {@link #setBuildDir(String)} is cleaned before CMake commands are called.
*
* @param value <code>true</code> if CMake should start in clean directory, <code>false</code> if
* it can contain files being there before.
*/
@Default(value = "false")
public void setCleanBuild(boolean value) {
this.cleanBuild = value;
}
/**
* @return the value set through {@link #setCleanBuild(boolean)}
*/
public boolean getCleanBuild() {
return this.cleanBuild;
}
/**
* Sets the maximum time of the build run [in seconds] from <code>timeout=""</code> attribute. The
* attribute is not required; if not specified, the builder can run forever ;-)
* .
* @param timeout time after which the build is terminated.
*/
public void setTimeout(long timeout) {
this.timeOut = timeout;
}
/**
* @return the value set through {@link #setTimeout(long)}
*/
public long getTimeout() {
return this.timeOut;
}
/**
* Creates object into which <code>{@code <option />}</code> tag will be set. Each call returns new
* object which is expected to be set by CC. The attribute is not required.
*
* @return new object to configure according to the tag values.
*/
public Object createOption() {
options.add(new Option());
return options.getLast();
}
/**
* Adds pre-configured object
* @param obj the pre-configured object (currently only instance of {@link CMakeBuilderOptions} class)
* @throws CruiseControlException
*/
@SkipDoc
public void add(Object obj) throws CruiseControlException {
/* Check the instance */
if (obj instanceof CMakeBuilderOptions) {
/* Add the maps to the list */
add((CMakeBuilderOptions) obj);
return;
}
/* Invalid object */
throw new CruiseControlException("Invalid configuration object: " + obj);
}
/**
* Adds pre-configured set of options which are merged with the current set of options set through {@link
* #createOption()}.
*
* @param optsobj the instance of {@link CMakeBuilderOptions} class
*/
protected void add(CMakeBuilderOptions optsobj) {
/* Add the options to the list */
for (Option o : optsobj.getOptions()) {
options.add(o);
}
/* Add the envs to the list */
for (EnvConf o : optsobj.getEnvs()) {
createEnv().copy(o);
}
}
/**
* Creates object representing the command run after the CMake is configured; such commands are for example
* <tt>make, make install</tt> and so on. Since the object is {@link ExecBuilder}, any command can be defined and
* all its attributes can be set.
*
* Attributes not set for the command are inherited from CMake configuration.
*
* @return the instance of {@link ExecBuilder} representing the command to execute
*/
@ManualChildName("ExecBuilder")
public ExecBuilderCMake createBuild() {
commands.add(createBuilder());
return commands.getLast();
}
/**
* Private wrapper for {@link #mergeEnv(OSEnvironment)} method` just calls the wrapped method. It is required
* in order to pass env variables to the individual builders.
*/
private void mergeEnv_wrap(@SuppressWarnings("javadoc") final OSEnvironment env) {
mergeEnv(env);
}
/** Creates new instance of ExecBuilder, in this case it is its ExecBuilderCMake override */
protected ExecBuilderCMake createBuilder() {
return new ExecBuilderCMake();
}
/** Returns iterable through builders created by {@link #createBuild()} */
protected Iterable<ExecBuilderCMake> getBuilders() {
return commands;
}
/** Returns iterable through options created by {@link #createOption()} */
protected Iterable<Option> getOptions() {
return options;
}
/* ----------- ATTRIBS BLOCK ----------- */
/** Serialization UID. */
private static final long serialVersionUID = -5848722491823283506L;
/** The value set in {@link #setBuildDir(String)}. */
private File buildDir;
/** The value set in {@link #setSrcRoot(String)}. */
private File srcRoot;
/** The value set in {@link #setCleanBuild(boolean)}. */
private boolean cleanBuild = false;
/** The maximum time of build run [in sec.]. */
private long timeOut = 0;
/** The list of <tt>-D</tt> defines passed to <tt>cmake</tt> command. */
private LinkedList<Option> options = new LinkedList<Option>();
/** The list of commands as they are executed one after another. */
private LinkedList<ExecBuilderCMake> commands = new LinkedList<ExecBuilderCMake>();
/** Logger. */
private static final Logger LOG = Logger.getLogger(CMakeBuilder.class);
/* ----------- NESTED CLASSES ----------- */
/**
* Class for the CMake <tt>option[=value]</tt> options configuration:
* <pre>
* {@code
* <option value="OPTION_NAME[=OPTION_VALUE]"/>
* }
* </pre>
*
* Not that '-' must be the part of option name!
*/
public static class Option extends StringWriter {
/**
* Sets the name of the option.
* @param option string with the define option name.
*/
public void setValue(String option) {
getBuffer().setLength(0);
append(option);
}
}
/**
* Wrapper of {@link ExecBuilder}.
* For version 2.8 and lower it has format:
* <pre>
* {@code
* cmake [options] <path-to-source>
* cmake [options] <path-to-existing-build>
* }
* </pre>
*
* Also, it calls {@link CMakeBuilder#mergeEnv(OSEnvironment)}
*/
protected class ExecBuilderCMake extends ExecBuilder {
/** Method adding single option to the list of arguments for CMake */
@SkipDoc
public void addOption(Option opt) {
addArg(opt.toString());
}
/** Method the last path argument for CMake, it is either <code>path-to-source</code> or
* <code>path-to-existing-build</code> */
@SkipDoc
public void addPath(File path) {
addArg(path.getAbsolutePath());
}
/** Overrides {@link #mergeEnv(OSEnvironment)} method to call parent's
* {@link CMakeBuilder#mergeEnv(OSEnvironment)} first, and its own implementation then */
@Override
public void mergeEnv(final OSEnvironment env) {
mergeEnv_wrap(env);
super.mergeEnv(env);
}
/** Adds single string option */
protected void addArg(final String arg) {
final String args = super.getArgs();
super.setArgs((args != null && args.length() > 0 ? args + " " : "") + arg);
}
/** Serialization UID */
private static final long serialVersionUID = -9071669502459334465L;
}
}