package net.sourceforge.cruisecontrol.builders;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
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.Description;
import net.sourceforge.cruisecontrol.gendoc.annotations.Required;
import net.sourceforge.cruisecontrol.util.Commandline;
import net.sourceforge.cruisecontrol.util.Directory;
import net.sourceforge.cruisecontrol.util.Util;
import org.apache.log4j.Logger;
import org.jdom.Element;
public class XcodeBuilder extends Builder implements Script {
private static final Logger LOG = Logger.getLogger(XcodeBuilder.class);
private static final String DEFAULT_OUTFILE_NAME = "xcodebuild.cc.output";
Directory directory = new Directory();
private int exitCode = -1;
private boolean hitBuildFailedMessage;
private long timeout = ScriptRunner.NO_TIMEOUT;
private boolean buildTimedOut;
private final Arguments arguments = new Arguments();
private Map<String, String> buildProperties;
@Override
public Element build(Map<String, String> properties, Progress progressIn) throws CruiseControlException {
final Progress progress = getShowProgress() ? progressIn : null;
// @todo To support progress, determine text pattern indicating progress messages in output,
// see AntScript.consumeLine() as an example
setProperties(properties);
OutputFile file = createOutputFile(directory, DEFAULT_OUTFILE_NAME);
runScript(file);
return elementFromFile(file);
}
void setProperties(Map<String, String> properties) {
buildProperties = properties;
}
private void runScript(OutputFile file) throws CruiseControlException {
LOG.info("starting build");
String projectName = buildProperties.get(Builder.BUILD_PROP_PROJECTNAME);
boolean finished = createScriptRunner().runScript(this, timeout,
getBuildOutputConsumer(projectName, file.file, file.file.getName()));
buildTimedOut = !finished;
LOG.info("build finished with exit code " + exitCode);
}
ScriptRunner createScriptRunner() {
return new ScriptRunner();
}
OutputFile createOutputFile(Directory d, String filename) {
return new OutputFile(d, filename);
}
Element elementFromFile(OutputFile file) {
hitBuildFailedMessage = false;
Element build = new Element("build");
while (file.hasMoreLines()) {
String line = file.nextLine();
Element message = getElementFromLine(line);
if (message != null) {
build.addContent(message);
}
}
if (hitBuildFailedMessage) {
build.setAttribute("error", "** BUILD FAILED **");
} else if (timeout != ScriptRunner.NO_TIMEOUT && buildTimedOut) {
build.setAttribute("error", "build timed out");
}
return build;
}
@Override
public Element buildWithTarget(Map<String, String> properties, String target, Progress progress)
throws CruiseControlException {
arguments.overrideTarget(target);
try {
return build(properties, progress);
} finally {
arguments.resetTarget();
}
}
@Override
public void validate() throws CruiseControlException {
LOG.debug("super.validate()");
super.validate();
LOG.debug("validate directory");
directory.validate();
LOG.debug("validate args");
arguments.validate();
}
public Commandline buildCommandline() throws CruiseControlException {
Commandline cmdLine = new Commandline();
cmdLine.setWorkingDir(directory);
cmdLine.setExecutable("xcodebuild");
arguments.addArguments(cmdLine, buildProperties);
return cmdLine;
}
public int getExitCode() {
return exitCode;
}
public void setExitCode(int result) {
LOG.debug("exit code set to " + result);
exitCode = result;
}
public void setDirectory(String path) {
directory.setPath(path);
}
public Element getElementFromLine(String line) {
if (hitBuildFailedMessage) {
return messageAtLevel(line, "error");
}
Element e = messageAtLevelIfContains(line, "warn", " warning: ");
if (e != null) {
return e;
}
e = messageAtLevelIfContains(line, "error", " error: ");
if (e != null) {
return e;
}
e = messageAtLevelIfContains(line, "error", "** BUILD FAILED **");
if (e != null) {
hitBuildFailedMessage = true;
return e;
}
return null;
}
private Element messageAtLevelIfContains(String line, String messageLevel, String semaphore) {
if (line.contains(semaphore)) {
return messageAtLevel(line, messageLevel);
}
return null;
}
private Element messageAtLevel(String line, String messageLevel) {
Element target = new Element("target");
Element task = new Element("task");
target.addContent(task);
Element message = new Element("message");
task.addContent(message);
message.setAttribute("priority", messageLevel);
message.setText(line);
return target;
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
static class OutputFile {
private final File file;
private BufferedReader reader;
private String nextLine;
OutputFile(Directory dir, String filename) {
file = new File(dir.toFile(), filename);
}
public String nextLine() {
return nextLine;
}
public boolean hasMoreLines() {
if (reader == null) {
createReader();
}
try {
nextLine = reader.readLine();
} catch (IOException e) {
LOG.error("error reading file " + file.getAbsolutePath(), e);
if (reader != null) {
try {
reader.close();
} catch (IOException ex) {
}
}
throw new RuntimeException(e);
}
if (nextLine != null) {
return true;
}
LOG.debug("reached end of build output");
try {
reader.close();
reader = null;
} catch (IOException ex) {
}
return false;
}
private void createReader() {
LOG.debug("creating reader for file " + file.getAbsolutePath());
try {
reader = new BufferedReader(new FileReader(file));
} catch (FileNotFoundException e) {
LOG.error("error creating reader for file " + file.getAbsolutePath(), e);
if (reader != null) {
try {
reader.close();
} catch (IOException ex) {
}
}
throw new RuntimeException(e);
}
}
}
@Description("Pass specified argument to xcodebuild. The element has the required attribute: value.")
public Arg createArg() {
return arguments.createArg();
}
class Arguments implements Serializable {
private static final long serialVersionUID = -6252774653884713089L;
private final List<Arg> args = new ArrayList<Arg>();
private Arg overrideTarget;
private Arg originalTarget;
public Arg createArg() {
Arg arg = new Arg();
args.add(arg);
return arg;
}
public void resetTarget() {
args.remove(overrideTarget);
if (originalTarget != null) {
args.add(originalTarget);
originalTarget = null;
}
}
public void overrideTarget(String string) {
saveOriginalTarget();
overrideTarget = createArg();
overrideTarget.setValue("-target " + string);
}
private void saveOriginalTarget() {
for (final Arg arg : args) {
if (arg.value.startsWith("-target ")) {
originalTarget = arg;
args.remove(arg);
break;
}
}
}
public void validate() throws CruiseControlException {
for (final Arg arg : args) {
arg.validate();
}
}
public void addArguments(Commandline cmdLine, Map<String, String> buildProperties) {
for (final Arg arg : args) {
String value = substituteProperties(buildProperties, arg.value);
cmdLine.createArgument().setValue(value);
}
}
private String substituteProperties(Map<String, String> properties, String string) {
String value = string;
try {
value = Util.parsePropertiesInString(properties, string, false);
} catch (CruiseControlException e) {
LOG.error("exception substituting properties into arguments: " + string, e);
}
return value;
}
}
@Description("Passes an argument to xcodebuild. Example: <code><arg value=\"-project "
+ "${projectname}\"></code>.")
public class Arg implements Serializable {
private static final long serialVersionUID = 832468395631809962L;
String value;
@Description("Argument to be passed on the command line. Any of the <a href=\""
+ "#buildproperties\">properties passed to builders</a> can "
+ "be substituted into the value.")
@Required
public void setValue(String value) {
this.value = value.trim();
}
public void validate() throws InvalidValueException {
if (value.length() == 0) {
throw new InvalidValueException();
}
}
class InvalidValueException extends CruiseControlException {
private static final long serialVersionUID = -1464414088701915032L;
InvalidValueException() {
super("value of arg can't be an empty string");
}
}
}
}