package hudson.plugins.nant; import hudson.CopyOnWrite; import hudson.Extension; import hudson.Functions; import hudson.Launcher; import hudson.Launcher.LocalLauncher; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.BuildListener; import hudson.model.Descriptor; import hudson.model.Hudson; import hudson.model.TaskListener; import hudson.remoting.Callable; import hudson.tasks.Builder; import hudson.util.ArgumentListBuilder; import hudson.util.FormValidation; import hudson.util.VariableResolver; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.util.Map; import net.sf.json.JSONObject; /** * Sample {@link Builder}. * * <p> * When the user configures the project and enables this builder, * {@link DescriptorImpl#newInstance(StaplerRequest)} is invoked * and a new {@link NantBuilder} is created. The created * instance is persisted to the project configuration XML by using * XStream, so this allows you to use instance fields (like {@link #nantName}) * to remember the configuration. * * <p> * When a build is performed, the {@link #perform(Build, Launcher, BuildListener)} method * will be invoked. * * @author kyle.sweeney@valtech.com * @author Justin Holzer (jsholzer@gmail.com) * */ public class NantBuilder extends Builder { /** * A whitespace separated list of nant targets to be run */ private final String targets; /** * The location of the nant build file to run */ private final String nantBuildFile; /** * Identifies {@link NantInstallation} to be used. */ private final String nantName; /** * The properties to pass to the NAnt build */ private final String properties; /** * When this builder is created in the project configuration step, * the builder object will be created from the strings below. * @param nantBuildFile The name/location of the nant build fild * @param targets Whitespace separated list of nant targets to run * @param properties property definitions (in Java properties format) */ @DataBoundConstructor public NantBuilder(String nantBuildFile,String nantName, String targets, String properties) { super(); if(nantBuildFile==null || nantBuildFile.trim().length()==0) this.nantBuildFile = ""; else this.nantBuildFile = nantBuildFile; this.nantName = nantName; if(targets == null || targets.trim().length()==0) this.targets = ""; else this.targets = targets; this.properties = Util.fixEmptyAndTrim(properties); } /** * Gets the NAnt to invoke, * or null to invoke the default one. */ public NantInstallation getNant() { for( NantInstallation i : DESCRIPTOR.getInstallations() ) { if(nantName!=null && i.getName().equals(nantName)) return i; } return null; } /** * We'll use these from the <tt>config.jelly</tt>. */ public String getTargets() { return targets; } public String getNantBuildFile(){ return nantBuildFile; } public String getNantName(){ return nantName; } public String getProperties() { return this.properties; } public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { ArgumentListBuilder args = new ArgumentListBuilder(); VariableResolver<String> vr = build.getBuildVariableResolver(); String execName = NantInstallation.getExecutableName(); //Get the path to the nant installation NantInstallation ni = getNant(); if(ni==null) { args.add(launcher.isUnix() ? NantConstants.NANT_EXECUTABLE_UNIX : NantConstants.NANT_EXECUTABLE_WINDOWS); } else { args.add(ni.getExecutable(launcher)); } // If a nant build file is specified, then add it as an argument, otherwise // nant will search for any file that ends in .build if(nantBuildFile != null && nantBuildFile.trim().length() > 0){ args.add("-buildfile:" + nantBuildFile); } // add the property declarations to the command line args.addKeyValuePairsFromPropertyString("-D:", properties, vr); // Remove all tabs, carriage returns, and newlines and replace them with // spaces, so that we can add them as parameters to the executable String normalizedTarget = targets.replaceAll("[\t\r\n]+"," "); if(normalizedTarget.trim().length()>0) args.addTokenized(normalizedTarget); //According to the Ant builder source code, in order to launch a program //from the command line in windows, we must wrap it into cmd.exe. This //way the return code can be used to determine whether or not the build failed. if(!launcher.isUnix()) { args.add("&&","exit","%%ERRORLEVEL%%"); // From hudson.tasks.Ant: // // on Windows, proper double quote handling requires extra surrounding quote. // so we need to convert the entire argument list once into a string, // then build the new list so that by the time JVM invokes CreateProcess win32 API, // it puts additional double-quote. See issue #1007 // the 'addQuoted' is necessary because Process implementation for Windows (at least in Sun JVM) // is too clever to avoid putting a quote around it if the argument begins with " // see "cmd /?" for more about how cmd.exe handles quotation. args = new ArgumentListBuilder().add("cmd.exe", "/C").addQuoted(args.toStringWithQuote()); } //Try to execute the command listener.getLogger().println("Executing command: " + args.toString()); Map<String,String> env = build.getEnvironment(listener); try { int r = launcher.launch().cmds(args).envs(env).stdout(listener).pwd(build.getModuleRoot()).join(); return r==0; } catch (IOException e) { Util.displayIOException(e,listener); e.printStackTrace( listener.fatalError("command execution failed") ); return false; } } @Override public Descriptor<Builder> getDescriptor() { // see Descriptor javadoc for more about what a descriptor is. return DESCRIPTOR; } /** * Descriptor should be singleton. */ @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); /** * Descriptor for {@link NantBuilder}. Used as a singleton. * The class is marked as public so that it can be accessed from views. */ public static final class DescriptorImpl extends Descriptor<Builder> { /** * To persist global configuration information, * simply store it in a field and call save(). * * <p> * If you don't want fields to be persisted, use <tt>transient</tt>. */ public static String PARAMETERNAME_PATH_TO_NANT = "pathToNant"; @CopyOnWrite private volatile NantInstallation[] installations = new NantInstallation[0]; private DescriptorImpl() { super(NantBuilder.class); load(); } protected void convert(Map<String,Object> oldPropertyBag) { if(oldPropertyBag.containsKey("installations")) installations = (NantInstallation[]) oldPropertyBag.get("installations"); } /** * This human readable name is used in the configuration screen. */ public String getDisplayName() { return Messages.NantBuilder_DisplayName(); } public NantInstallation[] getInstallations() { return installations; } @Override public boolean configure(StaplerRequest req, JSONObject formData) throws FormException{ // to persist global configuration information, // set that to properties and call save(). int i; String[] names = req.getParameterValues("nant_name"); String[] homes = req.getParameterValues("nant_home"); int len; if(names!=null && homes!=null) len = Math.min(names.length,homes.length); else len = 0; NantInstallation[] insts = new NantInstallation[len]; for( i=0; i<len; i++ ) { if(names[i].length()==0 || homes[i].length()==0) continue; insts[i] = new NantInstallation(names[i],homes[i]); } this.installations = insts; save(); return true; } // // web methods // /** * Checks if the NANT_HOME is valid. * @param value The value of the NANT_HOME field supplied by the user */ public FormValidation doCheckNantHome(@QueryParameter final String 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(); } File f = new File(Util.fixNull(value)); if(!f.isDirectory()) { return FormValidation.error(f+" is not a directory"); } NantInstallation nantInstall = new NantInstallation("", f.getAbsolutePath()); if(!nantInstall.getExecutableFile().exists()) { return FormValidation.error(f + " is not a NAnt installation directory."); } return FormValidation.ok(); } } public static final class NantInstallation implements Serializable { private final String name; private final String nantHome; public NantInstallation(String name, String nantHome) { this.name = name; this.nantHome = nantHome; } /** * install directory. */ public String getNantHome() { return nantHome; } /** * Human readable display name. */ public String getName() { return name; } @SuppressWarnings("serial") public String getExecutable(Launcher launcher) throws IOException, InterruptedException { return launcher.getChannel().call ( new Callable<String,IOException>() { public String call() throws IOException { File exe = getExecutableFile(); if(exe.exists()) return exe.getPath(); throw new IOException(exe.getPath() + " doesn't exist"); } } ); } /** * Returns the NAnt executable as a {@link File} * @return the NAnt executable as a {@link File} */ public File getExecutableFile() { return new File(getNantHome(), "bin/" + getExecutableName()); } /** * Returns the name of the NAnt executable for the current platform * @return the name of the NAnt executable for the current platform */ public static String getExecutableName() { if (Functions.isWindows()) return NantConstants.NANT_EXECUTABLE_WINDOWS; return NantConstants.NANT_EXECUTABLE_UNIX; } /** * Returns true if the executable exists. */ public boolean getExists() throws IOException, InterruptedException { LocalLauncher launcher = new LocalLauncher(TaskListener.NULL); return launcher.getChannel().call(new Callable<Boolean,IOException>() { public Boolean call() throws IOException { return getExecutableFile().exists(); } }); } private static final long serialVersionUID = 1L; } }