/*
* Copyright 2012, CloudBees Inc., Synopsys Inc. and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cloudbees.jenkins.plugins.customtools;
import com.synopsys.arc.jenkinsci.plugins.customtools.CustomToolsLogger;
import com.synopsys.arc.jenkinsci.plugins.customtools.CustomToolException;
import com.synopsys.arc.jenkinsci.plugins.customtools.EnvVariablesInjector;
import com.synopsys.arc.jenkinsci.plugins.customtools.LabelSpecifics;
import com.synopsys.arc.jenkinsci.plugins.customtools.PathsList;
import com.synopsys.arc.jenkinsci.plugins.customtools.multiconfig.MulticonfigWrapperOptions;
import com.synopsys.arc.jenkinsci.plugins.customtools.versions.ToolVersion;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.AbortException;
import hudson.EnvVars;
import hudson.Extension;
import hudson.Launcher;
import hudson.Proc;
import hudson.matrix.MatrixBuild;
import hudson.model.BuildListener;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Computer;
import hudson.model.Descriptor;
import hudson.model.Hudson;
import hudson.model.Node;
import hudson.model.Run.RunnerAbortedException;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrapperDescriptor;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.plugins.customtools.util.JenkinsHelper;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
/**
* Installs tools selected by the user. Exports configured paths and a home variable for each tool.
*
* @author rcampbell
* @author Oleg Nenashev
*
*/
public class CustomToolInstallWrapper extends BuildWrapper {
/**
* Ceremony needed to satisfy NoStaplerConstructionException:
* "There's no @DataBoundConstructor on any constructor of class java.lang.String"
* @author rcampbell
*
*/
public static class SelectedTool {
private final String name;
@DataBoundConstructor
public SelectedTool(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return name;
}
public @CheckForNull CustomTool toCustomTool() {
return ((CustomTool.DescriptorImpl)JenkinsHelper.getInstanceOrDie().getDescriptor(CustomTool.class)).byName(name);
}
public @Nonnull CustomTool toCustomToolValidated() throws CustomToolException {
CustomTool tool = toCustomTool();
if (tool == null) {
throw new CustomToolException(
Messages.CustomTool_GetToolByName_ErrorMessage(name));
}
return tool;
}
}
private @Nonnull SelectedTool[] selectedTools = new SelectedTool[0];
private final @CheckForNull MulticonfigWrapperOptions multiconfigOptions;
private final boolean convertHomesToUppercase;
@DataBoundConstructor
public CustomToolInstallWrapper(SelectedTool[] selectedTools, MulticonfigWrapperOptions multiconfigOptions, boolean convertHomesToUppercase) {
this.selectedTools = (selectedTools != null) ? selectedTools : new SelectedTool[0];
this.multiconfigOptions = (multiconfigOptions != null) ? multiconfigOptions : MulticonfigWrapperOptions.DEFAULT;
this.convertHomesToUppercase = convertHomesToUppercase;
}
public boolean isConvertHomesToUppercase() {
return convertHomesToUppercase;
}
@Override
public Environment setUp(AbstractBuild build, Launcher launcher,
BuildListener listener) throws IOException, InterruptedException {
final EnvVars buildEnv = build.getEnvironment(listener);
final Node node = build.getBuiltOn();
return new Environment() {
@Override
public void buildEnvVars(Map<String, String> env) {
// TODO: Inject Home dirs as well
for (SelectedTool selectedTool : selectedTools) {
CustomTool tool = selectedTool.toCustomTool();
if (tool != null && tool.hasVersions()) {
ToolVersion version = ToolVersion.getEffectiveToolVersion(tool, buildEnv, node);
if (version != null && !env.containsKey(version.getVariableName())) {
env.put(version.getVariableName(), version.getDefaultVersion());
}
}
}
}
};
}
public @Nonnull SelectedTool[] getSelectedTools() {
return selectedTools.clone();
}
/**
* The heart of the beast. Installs selected tools and exports their paths to the
* PATH and their HOMEs as environment variables.
* @return A decorated launcher
*/
@Override
public Launcher decorateLauncher(AbstractBuild build, final Launcher launcher,
BuildListener listener) throws IOException, InterruptedException,
RunnerAbortedException {
EnvVars buildEnv = build.getEnvironment(listener);
final EnvVars homes = new EnvVars();
final EnvVars versions = new EnvVars();
final PathsList paths = new PathsList();
final List<EnvVariablesInjector> additionalVarInjectors = new LinkedList<EnvVariablesInjector>();
// Handle multi-configuration build
if (build instanceof MatrixBuild) {
CustomToolsLogger.logMessage(listener, "Skipping installation of tools at the master job");
if (getMulticonfigOptions().isSkipMasterInstallation()) {
return launcher;
}
}
// Each tool can export zero or many directories to the PATH
final Node node = Computer.currentComputer().getNode();
if (node == null) {
throw new CustomToolException("Cannot install tools on the deleted node");
}
for (SelectedTool selectedToolName : selectedTools) {
CustomTool tool = selectedToolName.toCustomToolValidated();
CustomToolsLogger.logMessage(listener, tool.getName(), "Starting installation");
// Check versioning
checkVersions(tool, listener, buildEnv, node, versions);
// This installs the tool if necessary
CustomTool installed = tool
.forNode(node, listener)
.forEnvironment(buildEnv)
.forBuildProperties(build.getProject().getProperties());
try {
installed.check();
} catch (CustomToolException ex) {
throw new AbortException(ex.getMessage());
}
// Handle global options of the tool
//TODO: convert to label specifics?
final PathsList installedPaths = installed.getPaths(node);
installed.correctHome(installedPaths);
paths.add(installedPaths);
final String additionalVars = installed.getAdditionalVariables();
if (additionalVars != null) {
additionalVarInjectors.add(EnvVariablesInjector.create(additionalVars));
}
// Handle label-specific options of the tool
for (LabelSpecifics spec : installed.getLabelSpecifics()) {
if (!spec.appliesTo(node)) {
continue;
}
CustomToolsLogger.logMessage(listener, installed.getName(), "Label specifics from '"+spec.getLabel()+"' will be applied");
final String additionalLabelSpecificVars = spec.getAdditionalVars();
if (additionalLabelSpecificVars != null) {
additionalVarInjectors.add(EnvVariablesInjector.create(additionalLabelSpecificVars));
}
}
CustomToolsLogger.logMessage(listener, installed.getName(), "Tool is installed at "+ installed.getHome());
String homeDirVarName = (convertHomesToUppercase ? installed.getName().toUpperCase(Locale.ENGLISH) : installed.getName()) +"_HOME";
CustomToolsLogger.logMessage(listener, installed.getName(), "Setting "+ homeDirVarName+"="+installed.getHome());
homes.put(homeDirVarName, installed.getHome());
}
return new DecoratedLauncher(launcher) {
@Override
public Proc launch(ProcStarter starter) throws IOException {
EnvVars vars;
try { // Dirty hack, which allows to avoid NPEs in Launcher::envs()
vars = toEnvVars(starter.envs());
} catch (NullPointerException npe) {
vars = new EnvVars();
} catch (InterruptedException x) {
throw new IOException(x);
}
// Inject paths
final String injectedPaths = paths.toListString();
if (injectedPaths != null) {
vars.override("PATH+", injectedPaths);
}
// Inject additional variables
vars.putAll(homes);
vars.putAll(versions);
for (EnvVariablesInjector injector : additionalVarInjectors) {
injector.Inject(vars);
}
// Override paths to prevent JENKINS-20560
if (vars.containsKey("PATH")) {
final String overallPaths=vars.get("PATH");
vars.remove("PATH");
vars.put("PATH+", overallPaths);
}
return getInner().launch(starter.envs(vars));
}
private EnvVars toEnvVars(String[] envs) throws IOException, InterruptedException {
Computer computer = node.toComputer();
EnvVars vars = computer != null ? computer.getEnvironment() : new EnvVars();
for (String line : envs) {
vars.addLine(line);
}
return vars;
}
};
}
/**
* @deprecated The method is deprecated. It will be removed in future versions.
* @throws CustomToolException
*/
@SuppressFBWarnings(value = "NM_METHOD_NAMING_CONVENTION", justification = "Deprecated, will be removed later")
public void CheckVersions (CustomTool tool, BuildListener listener, EnvVars buildEnv, Node node, EnvVars target)
throws CustomToolException {
checkVersions(tool, listener, buildEnv, node, target);
}
/**
* Checks versions and modify build environment if required.
* @param tool Custom Tool
* @param listener Build Listener
* @param buildEnv Build Environment (can be modified)
* @param node Target Node
* @param target Build Internal Environment (can be modified)
* @throws CustomToolException
* @since 0.4
*/
public void checkVersions (@Nonnull CustomTool tool, @Nonnull BuildListener listener,
@Nonnull EnvVars buildEnv, @Nonnull Node node, @Nonnull EnvVars target) throws CustomToolException {
// Check version
if (tool.hasVersions()) {
ToolVersion version = ToolVersion.getEffectiveToolVersion(tool, buildEnv, node);
if (version == null) {
CustomToolsLogger.logMessage(listener, tool.getName(), "Error: No version has been specified, no default version. Failing the build...");
throw new CustomToolException("Version has not been specified for the "+tool.getName());
}
CustomToolsLogger.logMessage(listener, tool.getName(), "Version "+version.getActualVersion()+" has been specified by "+version.getVersionSource());
// Override default versions
final String versionSource = version.getVersionSource();
if (versionSource != null && versionSource.equals(ToolVersion.DEFAULTS_SOURCE)) {
String envStr = version.getVariableName()+"="+version.getDefaultVersion();
target.addLine(envStr);
buildEnv.addLine(envStr);
}
}
}
@Override
public Descriptor<BuildWrapper> getDescriptor() {
return DESCRIPTOR;
}
/**
* Check if build has specific multi-configuration options
* @return True if multi-configuration options are configured
* @since 0.3
*/
public boolean hasMulticonfigOptions() {
return multiconfigOptions != MulticonfigWrapperOptions.DEFAULT;
}
/**
* Gets multi-configuration job parameters.
* @return Configured options. Returns default options if not specified.
* @since 0.3
*/
public @Nonnull MulticonfigWrapperOptions getMulticonfigOptions() {
return multiconfigOptions != null ? multiconfigOptions : MulticonfigWrapperOptions.DEFAULT;
}
@Extension
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
public static final class DescriptorImpl extends BuildWrapperDescriptor {
public DescriptorImpl() {
super(CustomToolInstallWrapper.class);
}
@Override
public String getDisplayName() {
return Messages.Descriptor_DisplayName();
}
@Override
public boolean isApplicable(AbstractProject<?, ?> item) {
return true;
}
public CustomTool[] getInstallations() {
return JenkinsHelper.getInstanceOrDie().getDescriptorByType(CustomTool.DescriptorImpl.class).getInstallations();
}
@Override
public boolean configure(StaplerRequest req, JSONObject json)
throws hudson.model.Descriptor.FormException {
//TODO: Auto-generated method stub
return super.configure(req, json);
}
}
}