/******************************************************************************* * * 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(); } }