/*
* The MIT License
*
* Copyright (c) 2009, Romain Seguy
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Tom Huybrechts
*
* 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.plugins.radbuilder;
import hudson.CopyOnWrite;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
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.Descriptor.FormException;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.tools.ToolInstallation;
import hudson.util.ArgumentListBuilder;
import hudson.util.VariableResolver;
import java.io.IOException;
import net.sf.json.JSONObject;
import org.jvnet.localizer.ResourceBundleHolder;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
/**
* IBM Rational Application Developer builder, strongly based on the {@link Ant}
* one.
*
* <p>Note that, when this builder runs on Windows, there's actually a competition
* between Hudson's WORKSPACE environment variable and RAD's one, which is named
* exactly the same (with lower case letters, but Windows doesn't care). This
* confusion is dealt with at the beginning and at the end of the execution of
* the builder</p>
*
* @author Romain Seguy
* @version 1.1.2
*/
public class RAD extends Builder {
/**
* Name of the environment variable containing the RAD workspace to be used.
*/
public final static String RAD_WORKSPACE_ENV_VAR = "workspace";
/** Name of the .metadata folder of each RAD workspace. */
public final static String RAD_WORKSAPCE_METADATA_FOLDER = ".metadata";
/**
* When set to true, a new {@code PROJECT_WORKSPACE} environment variable
* will be created containing to replace the standard {@code WORKSPACE} one
* which is overwritten because of RAD.
*/
private final boolean activateProjectWorkspaceVar;
/** Optional build script path relative to the workspace. */
private final String buildFile;
/**
* Indicates that the content of the RAD workspace (including .metadata) has
* to be removed.
*/
private final boolean deleteRadWorkspaceContent;
/**
* Indicates that the .metadata folder of the RAD workspace has to be
* removed.
*/
private final boolean deleteRadWorkspaceMetadata;
/** Optional properties. */
private final String properties;
/** Identifies {@link RADInstallation} to be used. */
private final String radInstallationName;
/** Optional RAD workspace relative to the Hudson project's workspace */
private final String radWorkspace;
/**
* Optional lTargets, properties, and other Ant options, separated by
* whitespaces or newlines.
*/
private final String targets;
@DataBoundConstructor
public RAD(boolean activateProjectWorkspaceVar, String buildFile, boolean deleteRadWorkspaceContent, boolean deleteRadWorkspaceMetadata, String properties, String radInstallationName, String radWorkspace, String targets) {
this.activateProjectWorkspaceVar = activateProjectWorkspaceVar;
this.buildFile = Util.fixEmptyAndTrim(buildFile);
this.deleteRadWorkspaceContent = deleteRadWorkspaceContent;
this.deleteRadWorkspaceMetadata = deleteRadWorkspaceMetadata;
this.properties = Util.fixEmptyAndTrim(properties);
this.radInstallationName = radInstallationName;
this.radWorkspace = Util.fixEmptyAndTrim(radWorkspace);
this.targets = targets;
}
@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl) super.getDescriptor();
}
/**
* Returns the {@link RADInstallation} to use when the build takes place
* ({@code null} if none has been set).
*/
public RADInstallation getRadInstallation() {
for(RADInstallation installation: getDescriptor().getInstallations()) {
if(getRadInstallationName() != null && installation.getName().equals(getRadInstallationName())) {
return installation;
}
}
return null;
}
public boolean getActivateProjectWorkspaceVar() {
return activateProjectWorkspaceVar;
}
public String getBuildFile() {
return buildFile;
}
public boolean getDeleteRadWorkspaceContent() {
return deleteRadWorkspaceContent;
}
public boolean getDeleteRadWorkspaceMetadata() {
return deleteRadWorkspaceMetadata;
}
public String getProperties() {
return properties;
}
public String getRadInstallationName() {
return radInstallationName;
}
public String getRadWorkspace() {
return radWorkspace;
}
public String getTargets() {
return targets;
}
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
AbstractProject project = build.getProject();
ArgumentListBuilder args = new ArgumentListBuilder();
EnvVars env = build.getEnvironment(listener);
VariableResolver<String> varResolver = build.getBuildVariableResolver();
// --- RAD installation ---
// has a RAD installation been set? if yes, is it really a RAD installation?
RADInstallation radInstallation = getRadInstallation();
if(radInstallation == null) {
listener.fatalError(ResourceBundleHolder.get(RAD.class).format("NoInstallationSet"));
return false;
}
else {
radInstallation = radInstallation.forNode(Computer.currentComputer().getNode(), listener);
radInstallation = radInstallation.forEnvironment(env);
String runAntExecutable = radInstallation.getRunAntExecutable(launcher);
if(runAntExecutable == null) {
listener.fatalError(ResourceBundleHolder.get(RAD.class).format("NoRunAntExecutable", radInstallation.getName()));
return false;
}
args.add(runAntExecutable);
}
// --- RAD workspace ---
FilePath radWorkspaceFilePath;
if(getRadWorkspace() != null) {
// we're 100% sure that project.getWorkspace() is not null since we
// handle this case some lines ago
radWorkspaceFilePath = project.getWorkspace().child(getRadWorkspace());
// is the workspace specified by the user the same as the project's
// workspace?
if(radWorkspaceFilePath.getName().equals(project.getWorkspace().getName())) {
listener.fatalError("The RAD workspace is the same as the project's workspace: It must not.");
return false;
}
}
else {
radWorkspaceFilePath = project.getWorkspace().child(project.getName() + "-rad-workspace");
}
if(!radWorkspaceFilePath.exists()) {
// the workspace folder has to be created, other RAD will fail
radWorkspaceFilePath.mkdirs();
}
// we need to backup and remove Hudson's WORKSPACE variable to restore
// it at the end of the run
String hudsonWorkspaceEnvVar = env.get("WORKSPACE");
env.remove("WORKSPACE");
// do the user want to have a variable equivalent to WORKSPACE? if yes,
// PROJECT_WORKSPACE is created
if(getActivateProjectWorkspaceVar()) {
env.put("PROJECT_WORKSPACE", hudsonWorkspaceEnvVar.replaceAll("\\\\", "/")); // '\' will be changed to '/'
}
if(!launcher.isUnix()) {
// on Windows, we need the WORKSPACE var to be an absolute path,
// otherwise we get an "incorrect workspace=..." error
env.put(RAD_WORKSPACE_ENV_VAR, radWorkspaceFilePath.toURI().getPath().substring(1));
}
else {
env.put(RAD_WORKSPACE_ENV_VAR, radWorkspaceFilePath.getName());
}
if(getDeleteRadWorkspaceContent()) {
// we remove the whole content of the workspace, including .metadata
radWorkspaceFilePath.deleteContents();
}
else if(getDeleteRadWorkspaceMetadata()) {
// we only remove the .metadata folder
FilePath metadataFilePath = radWorkspaceFilePath.child(".metadata");
if(metadataFilePath.exists()) {
metadataFilePath.deleteRecursive();
}
}
// --- build file ---
String lBuildFile;
if(getBuildFile() == null) {
lBuildFile = "build.xml";
}
else {
lBuildFile = Util.replaceMacro(env.expand(getBuildFile()), varResolver);
}
if(project.getWorkspace() == null) {
listener.fatalError("Unable to find project's workspace");
return false;
}
FilePath buildFilePath = project.getWorkspace().child(lBuildFile);
if(!buildFilePath.exists()) {
listener.fatalError("Unable to find build script at " + buildFilePath);
return false;
}
args.add("-buildfile", buildFilePath.getName());
// --- properties ---
args.addKeyValuePairsFromPropertyString("-D", env.expand(getProperties()), varResolver);
// --- targets ---
String lTargets = Util.replaceMacro(env.expand(getTargets()), varResolver);
args.addTokenized(lTargets.replaceAll("[\t\r\n]+", " "));
if(!launcher.isUnix()) {
// on Windows, executing batch file can't return the correct error code,
// so we need to wrap it into cmd.exe.
// double %% is needed because we want ERRORLEVEL to be expanded after
// batch file executed, not before. This alone shows how broken Windows is...
args.add("&&", "exit", "%%ERRORLEVEL%%");
// 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());
}
long startTime = System.currentTimeMillis();
try {
int r = launcher.launch().cmds(args).envs(env).stdout(listener).pwd(buildFilePath.getParent()).join();
return r == 0;
}
catch(IOException ioe) {
Util.displayIOException(ioe, listener);
String errorMessage = ResourceBundleHolder.get(RAD.class).format("ExecutionFailed");
if(radInstallation == null && (System.currentTimeMillis()-startTime) < 1000) {
if(getDescriptor().getInstallations() == null) {
// no RAD installation has been set at all
errorMessage += ResourceBundleHolder.get(RAD.class).format("NoInstallationAtAll");
}
else {
errorMessage += ResourceBundleHolder.get(RAD.class).format("NoInstallationSet");
}
}
listener.fatalError(errorMessage);
return false;
}
finally {
// we need to restore Hudson's WORKSPACE environment variable
env.remove(RAD_WORKSPACE_ENV_VAR);
env.put("WORKSPACE", hudsonWorkspaceEnvVar);
env.remove("PROJECT_WORKSPACE");
}
}
@Extension
public static class DescriptorImpl extends BuildStepDescriptor<Builder> {
@CopyOnWrite
private volatile RADInstallation[] installations = new RADInstallation[0];
public DescriptorImpl() {
load();
}
protected DescriptorImpl(Class<? extends RAD> clazz) {
super(clazz);
}
@Override
public String getDisplayName() {
return ResourceBundleHolder.get(RAD.class).format("DisplayName");
}
public RADInstallation[] getInstallations() {
return installations;
}
/**
* Returns the {@link RADInstallation.DescriptorImpl} instance.
*/
public RADInstallation.DescriptorImpl getToolDescriptor() {
return ToolInstallation.all().get(RADInstallation.DescriptorImpl.class);
}
@Override
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
if(getInstallations() != null && getInstallations().length > 0) {
return true;
}
return false;
}
@Override
public Builder newInstance(StaplerRequest req, JSONObject formData) throws FormException {
return req.bindJSON(RAD.class, formData);
}
public void setInstallations(RADInstallation... installations) {
this.installations = installations;
save();
}
}
}