/* * The MIT License * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Jene Jasper, Stephen Connolly, Tom Huybrechts, Yahoo! Inc. * * 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.Extension; import jenkins.MasterToSlaveFileCallable; import hudson.Launcher; import hudson.Functions; import hudson.EnvVars; import hudson.Util; import hudson.CopyOnWrite; import hudson.Launcher.LocalLauncher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Computer; import hudson.model.EnvironmentSpecific; import hudson.model.Node; import jenkins.model.Jenkins; import jenkins.mvn.GlobalMavenConfig; import jenkins.mvn.GlobalSettingsProvider; import jenkins.mvn.SettingsProvider; import hudson.model.TaskListener; import hudson.remoting.VirtualChannel; import hudson.slaves.NodeSpecific; import hudson.tasks._maven.MavenConsoleAnnotator; 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.NullStream; import hudson.util.StreamTaskListener; import hudson.util.VariableResolver; import hudson.util.VariableResolver.ByMap; import hudson.util.VariableResolver.Union; import hudson.util.FormValidation; import hudson.util.XStream2; import jenkins.security.MasterToSlaveCallable; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import javax.annotation.Nonnull; import java.io.File; import java.io.IOException; import java.io.ObjectStreamException; import java.util.ArrayList; import java.util.Properties; import java.util.StringTokenizer; import java.util.List; import java.util.Collections; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.regex.Pattern; /** * Build by using Maven. * * @author Kohsuke Kawaguchi */ public class Maven extends Builder { /** * The targets and other maven options. * Can be separated by SP or NL. */ public final String targets; /** * Identifies {@link MavenInstallation} to be used. */ public final String mavenName; /** * MAVEN_OPTS if not null. */ public final String jvmOptions; /** * Optional POM file path relative to the workspace. * Used for the Maven '-f' option. */ public final String pom; /** * Optional properties to be passed to Maven. Follows {@link Properties} syntax. */ public final String properties; /** * If true, the build will use its own local Maven repository * via "-Dmaven.repo.local=...". * <p> * This would consume additional disk space, but provides isolation with other builds on the same machine, * such as mixing SNAPSHOTS. Maven also doesn't try to coordinate the concurrent access to Maven repositories * from multiple Maven process, so this helps there too. * * Identical to logic used in maven-plugin. * * @since 1.322 */ public boolean usePrivateRepository = false; /** * Provides access to the settings.xml to be used for a build. * @since 1.491 */ private SettingsProvider settings; /** * Provides access to the global settings.xml to be used for a build. * @since 1.491 */ private GlobalSettingsProvider globalSettings; /** * Skip injecting build variables as properties into maven process. * * Defaults to false unless user requests otherwise. Old configurations are set to true to mimic the legacy behaviour. * * @since TODO */ private @Nonnull Boolean injectBuildVariables; private final static String MAVEN_1_INSTALLATION_COMMON_FILE = "bin/maven"; private final static String MAVEN_2_INSTALLATION_COMMON_FILE = "bin/mvn"; private static final Pattern S_PATTERN = Pattern.compile("(^| )-s "); private static final Pattern GS_PATTERN = Pattern.compile("(^| )-gs "); public Maven(String targets,String name) { this(targets,name,null,null,null,false, null, null); } public Maven(String targets, String name, String pom, String properties, String jvmOptions) { this(targets, name, pom, properties, jvmOptions, false, null, null); } public Maven(String targets,String name, String pom, String properties, String jvmOptions, boolean usePrivateRepository) { this(targets, name, pom, properties, jvmOptions, usePrivateRepository, null, null); } public Maven(String targets,String name, String pom, String properties, String jvmOptions, boolean usePrivateRepository, SettingsProvider settings, GlobalSettingsProvider globalSettings) { this(targets, name, pom, properties, jvmOptions, usePrivateRepository, settings, globalSettings, false); } @DataBoundConstructor public Maven(String targets,String name, String pom, String properties, String jvmOptions, boolean usePrivateRepository, SettingsProvider settings, GlobalSettingsProvider globalSettings, boolean injectBuildVariables) { this.targets = targets; this.mavenName = name; this.pom = Util.fixEmptyAndTrim(pom); this.properties = Util.fixEmptyAndTrim(properties); this.jvmOptions = Util.fixEmptyAndTrim(jvmOptions); this.usePrivateRepository = usePrivateRepository; this.settings = settings != null ? settings : GlobalMavenConfig.get().getSettingsProvider(); this.globalSettings = globalSettings != null ? globalSettings : GlobalMavenConfig.get().getGlobalSettingsProvider(); this.injectBuildVariables = injectBuildVariables; } public String getTargets() { return targets; } /** * @since 1.491 */ public SettingsProvider getSettings() { return settings != null ? settings : GlobalMavenConfig.get().getSettingsProvider(); } protected void setSettings(SettingsProvider settings) { this.settings = settings; } /** * @since 1.491 */ public GlobalSettingsProvider getGlobalSettings() { return globalSettings != null ? globalSettings : GlobalMavenConfig.get().getGlobalSettingsProvider(); } protected void setGlobalSettings(GlobalSettingsProvider globalSettings) { this.globalSettings = globalSettings; } public void setUsePrivateRepository(boolean usePrivateRepository) { this.usePrivateRepository = usePrivateRepository; } public boolean usesPrivateRepository() { return usePrivateRepository; } @Restricted(NoExternalUse.class) // Exposed for view public boolean isInjectBuildVariables() { return injectBuildVariables; } /** * Gets the Maven to invoke, * or null to invoke the default one. */ public MavenInstallation getMaven() { for( MavenInstallation i : getDescriptor().getInstallations() ) { if(mavenName !=null && mavenName.equals(i.getName())) return i; } return null; } private Object readResolve() throws ObjectStreamException { if (injectBuildVariables == null) { injectBuildVariables = true; } return this; } /** * Looks for <tt>pom.xlm</tt> or <tt>project.xml</tt> to determine the maven executable * name. */ private static final class DecideDefaultMavenCommand extends MasterToSlaveFileCallable<String> { private static final long serialVersionUID = -2327576423452215146L; // command line arguments. private final String arguments; public DecideDefaultMavenCommand(String arguments) { this.arguments = arguments; } public String invoke(File ws, VirtualChannel channel) throws IOException { String seed=null; // check for the -f option StringTokenizer tokens = new StringTokenizer(arguments); while(tokens.hasMoreTokens()) { String t = tokens.nextToken(); if(t.equals("-f") && tokens.hasMoreTokens()) { File file = new File(ws,tokens.nextToken()); if(!file.exists()) continue; // looks like an error, but let the execution fail later seed = file.isDirectory() ? /* in M1, you specify a directory in -f */ "maven" /* in M2, you specify a POM file name. */ : "mvn"; break; } } if(seed==null) { // as of 1.212 (2008 April), I think Maven2 mostly replaced Maven1, so // switching to err on M2 side. seed = new File(ws,"project.xml").exists() ? "maven" : "mvn"; } return seed; } } @Override public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { VariableResolver<String> vr = build.getBuildVariableResolver(); EnvVars env = build.getEnvironment(listener); String targets = Util.replaceMacro(this.targets,vr); targets = env.expand(targets); String pom = env.expand(this.pom); int startIndex = 0; int endIndex; do { // split targets into multiple invokations of maven separated by | endIndex = targets.indexOf('|', startIndex); if (-1 == endIndex) { endIndex = targets.length(); } String normalizedTarget = targets .substring(startIndex, endIndex) .replaceAll("[\t\r\n]+"," "); ArgumentListBuilder args = new ArgumentListBuilder(); MavenInstallation mi = getMaven(); if(mi==null) { String execName = build.getWorkspace().act(new DecideDefaultMavenCommand(normalizedTarget)); args.add(execName); } else { mi = mi.forNode(Computer.currentComputer().getNode(), listener); mi = mi.forEnvironment(env); String exec = mi.getExecutable(launcher); if(exec==null) { listener.fatalError(Messages.Maven_NoExecutable(mi.getHome())); return false; } args.add(exec); } if(pom!=null) args.add("-f",pom); if(!S_PATTERN.matcher(targets).find()){ // check the given target/goals do not contain settings parameter already String settingsPath = SettingsProvider.getSettingsRemotePath(getSettings(), build, listener); if(StringUtils.isNotBlank(settingsPath)){ args.add("-s", settingsPath); } } if(!GS_PATTERN.matcher(targets).find()){ String settingsPath = GlobalSettingsProvider.getSettingsRemotePath(getGlobalSettings(), build, listener); if(StringUtils.isNotBlank(settingsPath)){ args.add("-gs", settingsPath); } } if (isInjectBuildVariables()) { Set<String> sensitiveVars = build.getSensitiveBuildVariables(); args.addKeyValuePairs("-D",build.getBuildVariables(),sensitiveVars); final VariableResolver<String> resolver = new Union<String>(new ByMap<String>(env), vr); args.addKeyValuePairsFromPropertyString("-D",this.properties,resolver,sensitiveVars); } if (usesPrivateRepository()) args.add("-Dmaven.repo.local=" + build.getWorkspace().child(".repository")); args.addTokenized(normalizedTarget); wrapUpArguments(args,normalizedTarget,build,launcher,listener); buildEnvVars(env, mi); if (!launcher.isUnix()) { args = args.toWindowsCommand(); } try { MavenConsoleAnnotator mca = new MavenConsoleAnnotator(listener.getLogger(),build.getCharset()); int r = launcher.launch().cmds(args).envs(env).stdout(mca).pwd(build.getModuleRoot()).join(); if (0 != r) { return false; } } catch (IOException e) { Util.displayIOException(e,listener); e.printStackTrace( listener.fatalError(Messages.Maven_ExecFailed()) ); return false; } startIndex = endIndex + 1; } while (startIndex < targets.length()); return true; } /** * Allows the derived type to make additional modifications to the arguments list. * * @since 1.344 */ protected void wrapUpArguments(ArgumentListBuilder args, String normalizedTarget, AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { } /** * Build up the environment variables toward the Maven launch. */ protected void buildEnvVars(EnvVars env, MavenInstallation mi) throws IOException, InterruptedException { if(mi!=null) { // if somebody has use M2_HOME they will get a classloading error // when M2_HOME points to a different version of Maven2 from // MAVEN_HOME (as Maven 2 gives M2_HOME priority.) // // The other solution would be to set M2_HOME if we are calling Maven2 // and MAVEN_HOME for Maven1 (only of use for strange people that // are calling Maven2 from Maven1) mi.buildEnvVars(env); } // just as a precaution // see http://maven.apache.org/continuum/faqs.html#how-does-continuum-detect-a-successful-build env.put("MAVEN_TERMINATE_CMD","on"); String jvmOptions = env.expand(this.jvmOptions); if(jvmOptions!=null) env.put("MAVEN_OPTS",jvmOptions.replaceAll("[\t\r\n]+"," ")); } @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl)super.getDescriptor(); } /** * @deprecated as of 1.286 * Use {@link jenkins.model.Jenkins#getDescriptorByType(Class)} to obtain the current instance. * For compatibility, this field retains the last created {@link DescriptorImpl}. * TODO: fix sonar plugin that depends on this. That's the only plugin that depends on this field. */ @Deprecated public static DescriptorImpl DESCRIPTOR; @Extension @Symbol("maven") public static final class DescriptorImpl extends BuildStepDescriptor<Builder> { @CopyOnWrite private volatile MavenInstallation[] installations = new MavenInstallation[0]; public DescriptorImpl() { DESCRIPTOR = this; load(); } public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } @Override public String getHelpFile(String fieldName) { if (fieldName != null && fieldName.equals("globalSettings")) fieldName = "settings"; // same help file return super.getHelpFile(fieldName); } public String getDisplayName() { return Messages.Maven_DisplayName(); } public GlobalSettingsProvider getDefaultGlobalSettingsProvider() { return GlobalMavenConfig.get().getGlobalSettingsProvider(); } public SettingsProvider getDefaultSettingsProvider() { return GlobalMavenConfig.get().getSettingsProvider(); } public MavenInstallation[] getInstallations() { return installations; } public void setInstallations(MavenInstallation... installations) { List<MavenInstallation> tmpList = new ArrayList<Maven.MavenInstallation>(); // remote empty Maven installation : if(installations != null) { Collections.addAll(tmpList, installations); for(MavenInstallation installation : installations) { if(Util.fixEmptyAndTrim(installation.getName()) == null) { tmpList.remove(installation); } } } this.installations = tmpList.toArray(new MavenInstallation[tmpList.size()]); save(); } @Override public Builder newInstance(StaplerRequest req, JSONObject formData) throws FormException { return req.bindJSON(Maven.class,formData); } } /** * Represents a Maven installation in a system. */ public static final class MavenInstallation extends ToolInstallation implements EnvironmentSpecific<MavenInstallation>, NodeSpecific<MavenInstallation> { /** * Constants for describing Maven versions for comparison. */ public static final int MAVEN_20 = 0; public static final int MAVEN_21 = 1; public static final int MAVEN_30 = 2; /** * @deprecated since 2009-02-25. */ @Deprecated // kept for backward compatibility - use getHome() private transient String mavenHome; /** * @deprecated as of 1.308. * Use {@link #Maven.MavenInstallation(String, String, List)} */ @Deprecated public MavenInstallation(String name, String home) { super(name, home); } @DataBoundConstructor public MavenInstallation(String name, String home, List<? extends ToolProperty<?>> properties) { super(Util.fixEmptyAndTrim(name), Util.fixEmptyAndTrim(home), properties); } /** * install directory. * * @deprecated as of 1.308. Use {@link #getHome()}. */ @Deprecated public String getMavenHome() { return getHome(); } public File getHomeDir() { return new File(getHome()); } @Override public void buildEnvVars(EnvVars env) { String home = getHome(); if (home == null) { return; } env.put("M2_HOME", home); env.put("MAVEN_HOME", home); env.put("PATH+MAVEN", home + "/bin"); } /** * Compares the version of this Maven installation to the minimum required version specified. * * @param launcher * Represents the node on which we evaluate the path. * @param mavenReqVersion * Represents the minimum required Maven version - constants defined above. */ public boolean meetsMavenReqVersion(Launcher launcher, int mavenReqVersion) throws IOException, InterruptedException { // FIXME using similar stuff as in the maven plugin could be better // olamy : but will add a dependency on maven in core -> so not so good String mavenVersion = launcher.getChannel().call(new MasterToSlaveCallable<String,IOException>() { private static final long serialVersionUID = -4143159957567745621L; public String call() throws IOException { File[] jars = new File(getHomeDir(),"lib").listFiles(); if(jars!=null) { // be defensive for (File jar : jars) { if (jar.getName().startsWith("maven-")) { JarFile jf = null; try { jf = new JarFile(jar); Manifest manifest = jf.getManifest(); String version = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION); if(version != null) return version; } finally { if(jf != null) jf.close(); } } } } return ""; } }); if (!mavenVersion.equals("")) { if (mavenReqVersion == MAVEN_20) { if(mavenVersion.startsWith("2.")) return true; } else if (mavenReqVersion == MAVEN_21) { if(mavenVersion.startsWith("2.") && !mavenVersion.startsWith("2.0")) return true; } else if (mavenReqVersion == MAVEN_30) { if(mavenVersion.startsWith("3.")) return true; } } return false; } /** * Is this Maven 2.1.x or 2.2.x - but not Maven 3.x? * * @param launcher * Represents the node on which we evaluate the path. */ public boolean isMaven2_1(Launcher launcher) throws IOException, InterruptedException { return meetsMavenReqVersion(launcher, MAVEN_21); } /** * Gets the executable path of this maven on the given target system. */ public String getExecutable(Launcher launcher) throws IOException, InterruptedException { return launcher.getChannel().call(new MasterToSlaveCallable<String,IOException>() { private static final long serialVersionUID = 2373163112639943768L; public String call() throws IOException { File exe = getExeFile("mvn"); if(exe.exists()) return exe.getPath(); exe = getExeFile("maven"); if(exe.exists()) return exe.getPath(); return null; } }); } private File getExeFile(String execName) { String m2Home = Util.replaceMacro(getHome(),EnvVars.masterEnvVars); if(Functions.isWindows()) { File exeFile = new File(m2Home, "bin/" + execName + ".bat"); // since Maven 3.3 .bat files are replaced with .cmd if (!exeFile.exists()) { return new File(m2Home, "bin/" + execName + ".cmd"); } return exeFile; } else { return new File(m2Home, "bin/" + execName); } } /** * Returns true if the executable exists. */ public boolean getExists() { try { return getExecutable(new LocalLauncher(new StreamTaskListener(new NullStream())))!=null; } catch (IOException | InterruptedException e) { return false; } } private static final long serialVersionUID = 1L; public MavenInstallation forEnvironment(EnvVars environment) { return new MavenInstallation(getName(), environment.expand(getHome()), getProperties().toList()); } public MavenInstallation forNode(Node node, TaskListener log) throws IOException, InterruptedException { return new MavenInstallation(getName(), translateFor(node, log), getProperties().toList()); } @Extension @Symbol("maven") public static class DescriptorImpl extends ToolDescriptor<MavenInstallation> { @Override public String getDisplayName() { return "Maven"; } @Override public List<? extends ToolInstaller> getDefaultInstallers() { return Collections.singletonList(new MavenInstaller(null)); } // overriding them for backward compatibility. // newer code need not do this @Override public MavenInstallation[] getInstallations() { return Jenkins.getInstance().getDescriptorByType(Maven.DescriptorImpl.class).getInstallations(); } // overriding them for backward compatibility. // newer code need not do this @Override public void setInstallations(MavenInstallation... installations) { Jenkins.getInstance().getDescriptorByType(Maven.DescriptorImpl.class).setInstallations(installations); } /** * Checks if the MAVEN_HOME is valid. */ @Override protected FormValidation checkHomeDirectory(File value) { File maven1File = new File(value,MAVEN_1_INSTALLATION_COMMON_FILE); File maven2File = new File(value,MAVEN_2_INSTALLATION_COMMON_FILE); if(!maven1File.exists() && !maven2File.exists()) return FormValidation.error(Messages.Maven_NotMavenDirectory(value)); return FormValidation.ok(); } } public static class ConverterImpl extends ToolConverter { public ConverterImpl(XStream2 xstream) { super(xstream); } @Override protected String oldHomeField(ToolInstallation obj) { return ((MavenInstallation)obj).mavenHome; } } } /** * Automatic Maven installer from apache.org. */ public static class MavenInstaller extends DownloadFromUrlInstaller { @DataBoundConstructor public MavenInstaller(String id) { super(id); } @Extension @Symbol("maven") public static final class DescriptorImpl extends DownloadFromUrlInstaller.DescriptorImpl<MavenInstaller> { public String getDisplayName() { return Messages.InstallFromApache(); } @Override public boolean isApplicable(Class<? extends ToolInstallation> toolType) { return toolType==MavenInstallation.class; } } } /** * Optional interface that can be implemented by {@link AbstractProject} * that has "contextual" {@link MavenInstallation} associated with it. * * <p> * Code like RedeployPublisher uses this interface in an attempt * to use the consistent Maven installation attached to the project. * * @since 1.235 */ public interface ProjectWithMaven { /** * Gets the {@link MavenInstallation} associated with the project. * Can be null. * * <p> * If the Maven installation can not be uniquely determined, * it's often better to return just one of them, rather than returning * null, since this method is currently ultimately only used to * decide where to parse <tt>conf/settings.xml</tt> from. */ MavenInstallation inferMavenInstallation(); } }