/*
* The MIT License
*
* Copyright (c) 2004-2011, Oracle Corporation, Kohsuke Kawaguchi, Tom Huybrechts, Yahoo! Inc.,
* Anton Kozak, Nikita Levyankov
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
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.ToolDescriptor;
import hudson.tools.ToolInstallation;
import hudson.tools.DownloadFromUrlInstaller;
import hudson.tools.ToolInstaller;
import hudson.tools.ToolProperty;
import hudson.util.ArgumentListBuilder;
import hudson.util.VariableResolver;
import hudson.util.FormValidation;
import hudson.util.XStream2;
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.StaplerRequest;
import org.kohsuke.stapler.QueryParameter;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Properties;
import java.util.List;
import java.util.Collections;
import java.util.Set;
/**
* 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;
@DataBoundConstructor
public Ant(String targets,String antName, String antOpts, String buildFile, String properties) {
this.targets = targets;
this.antName = antName;
this.antOpts = StringUtils.trimToNull(antOpts);
this.buildFile = StringUtils.trimToNull(buildFile);
this.properties = StringUtils.trimToNull(properties);
}
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 {
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();
}
}