All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hudsonci.maven.plugin.builder.internal.MavenProcessBuilder Maven / Gradle / Ivy

Go to download

This plug-in adds Maven3 support to Hudson. It adds a builder to Freestyle Project to build maven projects.

There is a newer version: 3.0.6
Show newest version
/**
 * The MIT License
 *
 * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved.
 *
 * 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 org.hudsonci.maven.plugin.builder.internal;

import org.hudsonci.utils.common.TestAccessible;
import org.hudsonci.maven.model.PropertiesDTO;
import org.hudsonci.maven.model.config.BuildConfigurationDTO;
import hudson.AbortException;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.util.ArgumentListBuilder;
import hudson.util.ClasspathBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.OutputStream;
import java.util.Map;

import static com.google.common.base.Preconditions.checkNotNull;
import static org.hudsonci.maven.eventspy.common.Constants.PORT_PROPERTY;
import static org.hudsonci.maven.model.ModelUtil.isEmpty;
import static org.hudsonci.maven.model.ModelUtil.isSet;
import static org.hudsonci.maven.plugin.builder.internal.MavenConstants.*;
import static org.hudsonci.maven.plugin.builder.internal.PathNormalizer.Platform.UNIX;
import static org.hudsonci.maven.plugin.builder.internal.PathNormalizer.Platform.WINDOWS;

/**
 * Helper to build the arguments and environment for launching Maven.
 *
 * @author Jason Dillon
 * @since 2.1.0
 */
public class MavenProcessBuilder
{
    private final Logger log;

    private boolean windows;

    private BuildConfigurationDTO config;

    private FilePath tmpDir;

    private FilePath mavenHome;

    private FilePath executable;

    private ClasspathBuilder extClasspath;

    private FilePath repository;

    private EnvVars env;

    private Map buildVariables;

    private Integer port;

    private FilePath workingDir;

    private OutputStream standardOutput;

    public MavenProcessBuilder(final Logger log) {
        this.log = checkNotNull(log);
    }

    @TestAccessible
    MavenProcessBuilder() {
        this.log = LoggerFactory.getLogger(getClass());
    }

    public MavenProcessBuilder withWindows(final boolean flag) {
        this.windows = flag;
        return this;
    }

    public MavenProcessBuilder withTmpDir(final FilePath dir) {
        this.tmpDir = dir;
        return this;
    }

    public MavenProcessBuilder withMavenHome(final FilePath dir) {
        this.mavenHome = dir;
        return this;
    }

    public MavenProcessBuilder withMavenExecutable(final FilePath file) {
        this.executable = file;
        return this;
    }

    public MavenProcessBuilder withExtClasspath(final ClasspathBuilder classpath) {
        this.extClasspath = classpath;
        return this;
    }

    public MavenProcessBuilder withRepository(final FilePath dir) {
        this.repository = dir;
        return this;
    }

    public MavenProcessBuilder withBuildVariables(final Map variables) {
        this.buildVariables = variables;
        return this;
    }

    public MavenProcessBuilder withEnv(final EnvVars env) {
        this.env = env;
        return this;
    }

    public MavenProcessBuilder withPort(final int port) {
        this.port = port;
        return this;
    }

    public MavenProcessBuilder withConfiguration(final BuildConfigurationDTO config) {
        this.config = config;
        return this;
    }

    public MavenProcessBuilder withWorkingDirectory(final FilePath dir) {
        this.workingDir = dir;
        return this;
    }

    public MavenProcessBuilder withStandardOutput(final OutputStream output) {
        this.standardOutput = output;
        return this;
    }

    private  T ensureNotNull(final T obj) {
        if (obj == null) {
            throw new IllegalStateException();
        }
        return obj;
    }

    /**
     * Allows sub-class to perform variable resolution.  All textual user-inputs should pass though this method.
     */
    protected String resolve(final String value) {
        return value;
    }

    private PathNormalizer normalizer;

    private PathNormalizer getNormalizer() {
        if (normalizer == null) {
            normalizer = new PathNormalizer(windows ? WINDOWS : UNIX);
        }
        return normalizer;
    }

    private String normalize(final String path) {
        return getNormalizer().normalize(path);
    }

    private FilePath normalize(final FilePath path) {
        return getNormalizer().normalize(path);
    }

    private ClasspathBuilder normalize(final ClasspathBuilder path) {
        return getNormalizer().normalize(path);
    }

    /**
     * Build the arguments.
     */
    public ArgumentListBuilder buildArguments() throws Exception {
        ensureNotNull(config);

        ArgumentListBuilder args = new ArgumentListBuilder();

        args.add(normalize(ensureNotNull(executable)));

        if (!isEmpty(config.getGoals())) {
            String goals = resolve(config.getGoals());
            args.addTokenized(goals);
            detectSuperfluousOptions(args);
            if (detectBannedOptions(args)) {
                // Do not attempt to go any further if we found banned options, the mvn execution won't work as expected
                throw new AbortException("Detected banned options");
            }
        }

        // Always show the version being used.
        args.add("-V");

        // Enable non-interactive (batch) mode.
        args.add("-B");

        if (isSet(config.getErrors())) {
            args.add("-e");
        }

        // FIXME: This does not properly resolve the values or trim them for spaces
        args.addKeyValuePairs("-D", ensureNotNull(buildVariables));

        if (config.getProperties() != null) {
            for (PropertiesDTO.Entry entry : config.getProperties().getEntries()) {
                String value = resolve(entry.getValue());
                value = value.trim();
                args.add(String.format("-D%s=%s", entry.getName(), value));
            }
        }

        // Just warn about banned properties, don't puke (may be setting some debug bits)
        detectBannedProperties(args);

        // Add eventspy configuration, needs to be here to avoid problems with spaces in paths
        args.add(String.format("-D%s=%s", MAVEN_EXT_CLASS_PATH, normalize(ensureNotNull(extClasspath)).toString(windows ? ";" : ":")));
        args.add(String.format("-D%s=%d", PORT_PROPERTY, ensureNotNull(port)));

        if (isSet(config.getPrivateRepository())) {
            // Complain when private repo configured as well as custom repository location via properties
            if (config.getProperties() != null && config.getProperties().contains(MAVEN_REPO_LOCAL)) {
                log.warn("Private repository configured as well as custom '{}' property; Custom property will take precedence", MAVEN_REPO_LOCAL);
            }
            else {
                args.add(String.format("-D%s=%s", MAVEN_REPO_LOCAL, normalize(ensureNotNull(repository))));
            }
        }

        if (!isEmpty(config.getPomFile())) {
            String file = resolve(config.getPomFile());
            args.add("-f").add(file);
        }

        if (isSet(config.getOffline())) {
            args.add("-o");
        }

        if (!isSet(config.getRecursive())) {
            args.add("-N");
        }

        if (config.getProfiles() != null) {
            for (String profile : config.getProfiles()) {
                profile = resolve(profile);
                args.add("-P").add(profile);
            }
        }

        if (config.getProjects() != null) {
            for (String project : config.getProjects()) {
                project = resolve(project);
                args.add("-pl").add(project);
            }
        }

        if (!isEmpty(config.getResumeFrom())) {
            String resumeFrom = resolve(config.getResumeFrom());
            args.add("-rf").add(resumeFrom);
        }

        if (config.getVerbosity() != null) {
            switch (config.getVerbosity()) {
                case QUIET:
                    args.add("-q");
                    break;
                case DEBUG:
                    args.add("-X");
                    break;
            }
        }

        if (config.getChecksumMode() != null) {
            switch (config.getChecksumMode()) {
                case LAX:
                    args.add("-c");
                    break;
                case STRICT:
                    args.add("-C");
            }
        }

        if (config.getFailMode() != null) {
            switch (config.getFailMode()) {
                case FAST:
                    args.add("-ff");
                    break;
                case AT_END:
                    args.add("-fae");
                    break;
                case NEVER:
                    args.add("-fn");
                    break;
            }
        }

        if (config.getMakeMode() != null) {
            switch (config.getMakeMode()) {
                case DEPENDENCIES:
                    args.add("-am");
                    break;
                case DEPENDENTS:
                    args.add("-amd");
                    break;
                case BOTH:
                    args.add("-am").add("-amd");
                    break;
            }
        }

        if (config.getSnapshotUpdateMode() != null) {
            switch (config.getSnapshotUpdateMode()) {
                case FORCE:
                    args.add("-U");
                    break;
                case SUPPRESS:
                    args.add("-nsu");
                    break;
            }
        }

        if (!isEmpty(config.getThreading())) {
            String threading = resolve(config.getThreading());
            args.add("-T").add(threading);
        }

        if (log.isDebugEnabled()) {
            log.debug("Arguments:");
            for (String arg : args.toCommandArray()) {
                log.debug("  '{}'", arg);
            }
        }

        return args;
    }

    /**
     * Build MAVEN_OPTS values.
     */
    public ArgumentListBuilder buildOpts() throws Exception {
        ensureNotNull(config);

        ArgumentListBuilder mavenOpts = new ArgumentListBuilder();

        if (!isEmpty(config.getMavenOpts())) {
            String opts = resolve(config.getMavenOpts());
            mavenOpts.addTokenized(opts);
            detectSuperfluousOptions(mavenOpts);
            detectBannedProperties(mavenOpts);
            if (detectBannedOptions(mavenOpts)) {
                // Do not attempt to go any further if we found banned options, the mvn execution won't work as expected
                throw new AbortException("Detected banned options");
            }
        }

        if (isSet(config.getPrivateTmpdir())) {
            // Add tmpdir if not already configured
            String customTmpdir = findArgumentWithPrefix(mavenOpts, String.format("-D%s=", JAVA_IO_TMPDIR));
            if (customTmpdir != null) {
                log.warn("Using user-configured tmpdir: {}", customTmpdir);
            }
            else {
                if (tmpDir != null) {
                    String path = tmpDir.getRemote();
                    if (path.contains(" ")) {
                        log.warn("Path contains spaces: {}", path);
                    }

                    // Ensure that the path has a trailing separator char, some bits look like they expect it (hsperfdata* tmpdir)
                    String sep = windows ? "\\" : "/";
                    if (!path.endsWith(sep)) {
                        path += sep;
                    }

                    mavenOpts.add(String.format("-D%s=%s", JAVA_IO_TMPDIR, normalize(path)));
                }
                else {
                    log.warn("Using default tmpdir");
                }
            }
        }

        return mavenOpts;
    }

    private String findArgumentWithPrefix(final ArgumentListBuilder args, final String prefix) {
        for (String arg : args.toList()) {
            if (arg.startsWith(prefix)) {
                return arg;
            }
        }
        return null;
    }

    /**
     * Build the environment.
     */
    public EnvVars buildEnv() throws Exception {
        ensureNotNull(env);

        // These are for windows only
        if (windows) {
            env.put(MAVEN_TERMINATE_CMD, ON);
            env.put(MAVEN_BATCH_ECHO, OFF);
            env.put(MAVEN_BATCH_PAUSE, OFF);
        }

        env.put(M2_HOME, normalize(ensureNotNull(mavenHome)).getRemote());
        env.put(MAVEN_OPTS, buildOpts().toStringWithQuote());
        env.put(MAVEN_SKIP_RC, TRUE);

        if (log.isDebugEnabled()) {
            log.debug("Environment:");
            for (Map.Entry entry : env.entrySet()) {
                log.debug("  {}='{}'", entry.getKey(), entry.getValue());
            }
        }

        return env;
    }

    /**
     * Build the process starter.
     */
    public Launcher.ProcStarter build(final Launcher.ProcStarter starter) throws Exception {
        assert starter != null;

        return starter
            .cmds(buildArguments())
            .envs(buildEnv())
            .pwd(ensureNotNull(workingDir))
            .stdout(ensureNotNull(standardOutput));
    }

    // FIXME: Due to the use of commons-cli which does not require the fully qualified option name to be used,
    // FIXME: ... these checks will fail to catch some uses of options.  May need to either use commons-cli here
    // FIXME: ... to validate or figure out what the unique prefix for each is and check for arg startsWith()

    /**
     * Superfluous options, users should be warned when configuring these via goals or mavenOpts, as they are pointless.
     */
    public static final String[] SUPERFLUOUS_OPTIONS = {
        "-B",
        "--batch-mode",
        "-batch-mode",
        "-V",
        "--show-version",
        "-show-version"
        // Maven will also warn about deprecated options, so no need to add checks for them here
    };

    /**
     * Complain if any superfluous options were configured.
     */
    boolean detectSuperfluousOptions(final ArgumentListBuilder args) {
        assert args != null;
        boolean detected = false;
        for (String arg : args.toCommandArray()) {
            for (String opt : SUPERFLUOUS_OPTIONS) {
                if (arg.equals(opt)) {
                    log.warn("Detected superfluous option: {}", arg);
                    detected = true;
                }
            }
        }
        return detected;
    }

    /**
     * Banned options, users should be warned when configuring these via goals or mavenOpts, as this will prevent the integration from functioning
     * correctly.
     */
    public static final String[] BANNED_OPTIONS = {
        "--help",
        "-help",
        "-h",
        "--version",
        "-version",
        "-v",
        "-ep",
        "--encrypt-password",
        "-encrypt-password",
        "-emp",
        "--encrypt-master-password",
        "-encrypt-master-password"
    };

    /**
     * Complain if any banned options were configured.
     */
    boolean detectBannedOptions(final ArgumentListBuilder args) {
        assert args != null;
        boolean detected = false;
        for (String arg : args.toCommandArray()) {
            for (String opt : BANNED_OPTIONS) {
                if (arg.equals(opt)) {
                    log.error("Detected banned option: {}", arg);
                    detected = true;
                }
            }
        }
        return detected;
    }

    /**
     * Banned properties (prefixes in arg form), users should be warned when configuring these via goals or mavenOpts, as they may prevent the
     * integration from functioning correctly.
     */
    public static final String[] BANNED_PROPERTIES = {
        "-Dmaven.ext.class.path=",
        "-Dhudson.eventspy."
    };

    /**
     * Complain if any banned properties were configured.
     */
    public boolean detectBannedProperties(final ArgumentListBuilder args) {
        assert args != null;
        boolean detected = false;
        for (String arg : args.toCommandArray()) {
            for (String prop : BANNED_PROPERTIES) {
                if (arg.startsWith(prop)) {
                    log.warn("Detected banned property: {}", arg);
                    detected = true;
                }
            }
        }
        return detected;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy