/*******************************************************************************
*
* Copyright (c) 2004-2011 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, Tom Huybrechts, Yahoo! Inc., Nikita Levyankov
*
*
*******************************************************************************/
package hudson.tasks;
import hudson.CopyOnWrite;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Functions;
import hudson.Launcher;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Computer;
import hudson.model.EnvironmentSpecific;
import hudson.model.Hudson;
import hudson.model.Node;
import hudson.model.TaskListener;
import hudson.remoting.Callable;
import hudson.slaves.NodeSpecific;
import hudson.tasks._ant.AntConsoleAnnotator;
import hudson.tools.DownloadFromUrlInstaller;
import hudson.tools.ToolDescriptor;
import hudson.tools.ToolInstallation;
import hudson.tools.ToolInstaller;
import hudson.tools.ToolProperty;
import hudson.util.ArgumentListBuilder;
import hudson.util.FormValidation;
import hudson.util.VariableResolver;
import hudson.util.XStream2;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import net.sf.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
/**
* Ant launcher.
*
* @author Kohsuke Kawaguchi
*/
public class Ant extends Builder {
/**
* The targets, properties, and other Ant options. Either separated by
* whitespace or newline.
*/
private final String targets;
/**
* Identifies {@link AntInstallation} to be used.
*/
private final String antName;
/**
* ANT_OPTS if not null.
*/
private final String antOpts;
/**
* Optional build script path relative to the workspace. Used for the Ant
* '-f' option.
*/
private final String buildFile;
/**
* Optional properties to be passed to Ant. Follows {@link Properties}
* syntax.
*/
private final String properties;
public Ant(String targets, String antName, String antOpts, String buildFile, String properties) {
this(targets, antName, antOpts, buildFile, properties, false, "");
}
@DataBoundConstructor
public Ant(String targets, String antName, String antOpts, String buildFile, String properties, boolean disabled, String description) {
this.targets = targets;
this.antName = antName;
this.antOpts = StringUtils.trimToNull(antOpts);
this.buildFile = StringUtils.trimToNull(buildFile);
this.properties = StringUtils.trimToNull(properties);
setDisabled(disabled);
setDescription(description);
}
public String getBuildFile() {
return buildFile;
}
public String getProperties() {
return properties;
}
public String getTargets() {
return targets;
}
/**
* Gets the Ant to invoke, or null to invoke the default one.
*/
public AntInstallation getAnt() {
for (AntInstallation i : getDescriptor().getInstallations()) {
if (antName != null && antName.equals(i.getName())) {
return i;
}
}
return null;
}
/**
* Gets the ANT_OPTS parameter, or null.
*/
public String getAntOpts() {
return antOpts;
}
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
if (isDisabled()){
listener.getLogger().println("\nThe Ant builder is temporarily disabled.\n");
// just continue, this builder is disabled temporarily
return true;
}
ArgumentListBuilder args = new ArgumentListBuilder();
EnvVars env = build.getEnvironment(listener);
AntInstallation ai = getAnt();
if (ai == null) {
args.add(launcher.isUnix() ? "ant" : "ant.bat");
} else {
ai = ai.forNode(Computer.currentComputer().getNode(), listener);
ai = ai.forEnvironment(env);
String exe = ai.getExecutable(launcher);
if (exe == null) {
listener.fatalError(Messages.Ant_ExecutableNotFound(ai.getName()));
return false;
}
args.add(exe);
}
VariableResolver<String> vr = build.getBuildVariableResolver();
String buildFile = env.expand(this.buildFile);
String targets = Util.replaceMacro(env.expand(this.targets), vr);
FilePath buildFilePath = buildFilePath(build.getModuleRoot(), buildFile, targets);
if (!buildFilePath.exists()) {
// because of the poor choice of getModuleRoot() with CVS/Subversion, people often get confused
// with where the build file path is relative to. Now it's too late to change this behavior
// due to compatibility issue, but at least we can make this less painful by looking for errors
// and diagnosing it nicely. See HUDSON-1782
// first check if this appears to be a valid relative path from workspace root
FilePath buildFilePath2 = buildFilePath(build.getWorkspace(), buildFile, targets);
if (buildFilePath2.exists()) {
// This must be what the user meant. Let it continue.
buildFilePath = buildFilePath2;
} else {
// neither file exists. So this now really does look like an error.
listener.fatalError("Unable to find build script at " + buildFilePath);
return false;
}
}
if (buildFile != null) {
args.add("-file", buildFilePath.getName());
}
Set<String> sensitiveVars = build.getSensitiveBuildVariables();
args.addKeyValuePairs("-D", build.getBuildVariables(), sensitiveVars);
args.addKeyValuePairsFromPropertyString("-D", properties, vr, sensitiveVars);
args.addTokenized(targets.replaceAll("[\t\r\n]+", " "));
if (ai != null) {
env.put("ANT_HOME", ai.getHome());
}
if (antOpts != null) {
env.put("ANT_OPTS", env.expand(antOpts));
}
if (!launcher.isUnix()) {
args = args.toWindowsCommand();
// For some reason, ant on windows rejects empty parameters but unix does not.
// Add quotes for any empty parameter values:
List<String> newArgs = new ArrayList<String>(args.toList());
newArgs.set(newArgs.size() - 1, newArgs.get(newArgs.size() - 1).replaceAll(
"(?<= )(-D[^\" ]+)= ", "$1=\"\" "));
args = new ArgumentListBuilder(newArgs.toArray(new String[newArgs.size()]));
}
long startTime = System.currentTimeMillis();
try {
AntConsoleAnnotator aca = new AntConsoleAnnotator(listener.getLogger(), build.getCharset());
int r;
try {
r = launcher.launch().cmds(args).envs(env).stdout(aca).pwd(buildFilePath.getParent()).join();
} finally {
aca.forceEol();
}
return r == 0;
} catch (IOException e) {
Util.displayIOException(e, listener);
String errorMessage = Messages.Ant_ExecFailed();
if (ai == null && (System.currentTimeMillis() - startTime) < 1000) {
if (getDescriptor().getInstallations() == null) // looks like the user didn't configure any Ant installation
{
errorMessage += Messages.Ant_GlobalConfigNeeded();
} else // There are Ant installations configured but the project didn't pick it
{
errorMessage += Messages.Ant_ProjectConfigNeeded();
}
}
e.printStackTrace(listener.fatalError(errorMessage));
return false;
}
}
private static FilePath buildFilePath(FilePath base, String buildFile, String targets) {
if (buildFile != null) {
return base.child(buildFile);
}
// some users specify the -f option in the targets field, so take that into account as well.
// see
String[] tokens = Util.tokenize(targets);
for (int i = 0; i < tokens.length - 1; i++) {
String a = tokens[i];
if (a.equals("-f") || a.equals("-file") || a.equals("-buildfile")) {
return base.child(tokens[i + 1]);
}
}
return base.child("build.xml");
}
@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl) super.getDescriptor();
}
@Extension
public static class DescriptorImpl extends BuildStepDescriptor<Builder> {
@CopyOnWrite
private volatile AntInstallation[] installations = new AntInstallation[0];
public DescriptorImpl() {
load();
}
protected DescriptorImpl(Class<? extends Ant> clazz) {
super(clazz);
}
/**
* Obtains the {@link AntInstallation.DescriptorImpl} instance.
*/
public AntInstallation.DescriptorImpl getToolDescriptor() {
return ToolInstallation.all().get(AntInstallation.DescriptorImpl.class);
}
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return true;
}
@Override
public String getHelpFile() {
return "/help/project-config/ant.html";
}
public String getDisplayName() {
return Messages.Ant_DisplayName();
}
public AntInstallation[] getInstallations() {
return installations;
}
@Override
public Ant newInstance(StaplerRequest req, JSONObject formData) throws FormException {
return (Ant) req.bindJSON(clazz, formData);
}
public void setInstallations(AntInstallation... antInstallations) {
this.installations = antInstallations;
save();
}
}
/**
* Represents the Ant installation on the system.
*/
public static final class AntInstallation extends ToolInstallation implements
EnvironmentSpecific<AntInstallation>, NodeSpecific<AntInstallation> {
// to remain backward compatible with earlier Hudson that stored this field here.
@Deprecated
private transient String antHome;
@DataBoundConstructor
public AntInstallation(String name, String home, List<? extends ToolProperty<?>> properties) {
super(name, launderHome(home), properties);
}
/**
* @deprecated as of 1.308 Use
* {@link #AntInstallation(String, String, List)}
*/
public AntInstallation(String name, String home) {
this(name, home, Collections.<ToolProperty<?>>emptyList());
}
private static String launderHome(String home) {
if (home.endsWith("/") || home.endsWith("\\")) {
// see https://issues.apache.org/bugzilla/show_bug.cgi?id=26947
// Ant doesn't like the trailing slash, especially on Windows
return home.substring(0, home.length() - 1);
} else {
return home;
}
}
/**
* install directory.
*
* @deprecated as of 1.307. Use {@link #getHome()}.
*/
public String getAntHome() {
return getHome();
}
/**
* Gets the executable path of this Ant on the given target system.
*/
public String getExecutable(Launcher launcher) throws IOException, InterruptedException {
return launcher.getChannel().call(new Callable<String, IOException>() {
public String call() throws IOException {
File exe = getExeFile();
if (exe.exists()) {
return exe.getPath();
}
return null;
}
});
}
private File getExeFile() {
String execName = Functions.isWindows() ? "ant.bat" : "ant";
String home = Util.replaceMacro(getHome(), EnvVars.masterEnvVars);
return new File(home, "bin/" + execName);
}
/**
* Returns true if the executable exists.
*/
public boolean getExists() throws IOException, InterruptedException {
return getExecutable(new Launcher.LocalLauncher(TaskListener.NULL)) != null;
}
private static final long serialVersionUID = 1L;
public AntInstallation forEnvironment(EnvVars environment) {
return new AntInstallation(getName(), environment.expand(getHome()), getProperties().toList());
}
public AntInstallation forNode(Node node, TaskListener log) throws IOException, InterruptedException {
return new AntInstallation(getName(), translateFor(node, log), getProperties().toList());
}
@Extension
public static class DescriptorImpl extends ToolDescriptor<AntInstallation> {
@Override
public String getDisplayName() {
return "Ant";
}
// for compatibility reasons, the persistence is done by Ant.DescriptorImpl
@Override
public AntInstallation[] getInstallations() {
return Hudson.getInstance().getDescriptorByType(Ant.DescriptorImpl.class).getInstallations();
}
@Override
public void setInstallations(AntInstallation... installations) {
Hudson.getInstance().getDescriptorByType(Ant.DescriptorImpl.class).setInstallations(installations);
}
@Override
public List<? extends ToolInstaller> getDefaultInstallers() {
return Collections.singletonList(new AntInstaller(null));
}
/**
* Checks if the ANT_HOME is valid.
*/
public FormValidation doCheckHome(@QueryParameter File value) {
// this can be used to check the existence of a file on the server, so needs to be protected
if (!Hudson.getInstance().hasPermission(Hudson.ADMINISTER)) {
return FormValidation.ok();
}
if (value.getPath().equals("")) {
return FormValidation.ok();
}
if (!value.isDirectory()) {
return FormValidation.error(Messages.Ant_NotADirectory(value));
}
File antJar = new File(value, "lib/ant.jar");
if (!antJar.exists()) {
return FormValidation.error(Messages.Ant_NotAntDirectory(value));
}
return FormValidation.ok();
}
public FormValidation doCheckName(@QueryParameter String value) {
return FormValidation.validateRequired(value);
}
}
public static class ConverterImpl extends ToolConverter {
public ConverterImpl(XStream2 xstream) {
super(xstream);
}
@Override
protected String oldHomeField(ToolInstallation obj) {
return ((AntInstallation) obj).antHome;
}
}
}
/**
* Automatic Ant installer from apache.org.
*/
public static class AntInstaller extends DownloadFromUrlInstaller {
@DataBoundConstructor
public AntInstaller(String id) {
super(id);
}
@Extension
public static final class DescriptorImpl extends DownloadFromUrlInstaller.DescriptorImpl<AntInstaller> {
public String getDisplayName() {
return Messages.InstallFromApache();
}
@Override
public boolean isApplicable(Class<? extends ToolInstallation> toolType) {
return toolType == AntInstallation.class;
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Ant that = (Ant) o;
return new EqualsBuilder()
.append(antName, that.antName)
.append(antOpts, that.antOpts)
.append(buildFile, that.buildFile)
.append(properties, that.properties)
.append(targets, that.targets)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder()
.append(targets)
.append(antName)
.append(antOpts)
.append(buildFile)
.append(properties)
.toHashCode();
}
}