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

org.digitalmediaserver.nsis.MakeMojo Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2018 Digital Media Server
 *
 * 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 org.digitalmediaserver.nsis;

import static org.digitalmediaserver.nsis.Utils.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.digitalmediaserver.nsis.io.ProcessOutputConsumer;
import org.digitalmediaserver.nsis.io.ProcessOutputHandler;

/**
 * Compiles a NSIS script and builds a Windows executable.
 *
 * @author Nadahar
 */
@Mojo(name = "make", defaultPhase = LifecyclePhase.PACKAGE)
public class MakeMojo extends AbstractMojo implements ProcessOutputConsumer {

	/** The static Windows line separator sequence */
	public static final String WINDOWS_LINE_SEPARATOR = "\r\n";

	/** The default {@link CompressOption} */
	public static final  CompressOption DEFAULT_COMPRESS = CompressOption.auto;

	/** The default compression {@link CompressionType} */
	public static final CompressionType DEFAULT_COMPRESSION = CompressionType.zlib;

	/** The default {@link CompressionType#lzma} dictionary size */
	public static final int DEFAULT_LZMA_DICT_SIZE = 8;

	/**
	 * Whether or not {@link #outputFile} should be attached to the Maven build.
	 * You probably want an installer to be attached, but if you build another
	 * executable that might not be the case.
	 */
	@Parameter(property = "nsis.attachArtifact", defaultValue = "true")
	private boolean attachArtifact;

	/**
	 * Whether or not to automatically set the {@code NSISDIR} environment
	 * variable based on the folder where the {@code makensis} executable is
	 * located. Useful when {@code makensis} is compiled with
	 * {@code NSIS_CONFIG_CONST_DATA_PATH=no}.
	 */
	@Parameter(property = "nsis.auto.nsisdir", defaultValue = "true", required = true)
	private boolean autoNsisDir;

	/** The classifier to append to {@link outputFile}'s name. */
	@Parameter(property = "nsis.classifier")
	private String classifier;

	/**
	 * The {@link CompressOption} to apply to {@link #scriptFile}.
	 */
	@Parameter(property = "nsis.compress")
	private CompressOption compress;

	/**
	 * The {@link CompressionType} to apply to {@link #scriptFile}.
	 */
	@Parameter(property = "nsis.compression")
	private CompressionType compression;

	/**
	 * The dictionary size to use if {@link #compression} is
	 * {@link CompressionType#lzma}. Defaults to
	 * {@value #DEFAULT_LZMA_DICT_SIZE}.
	 */
	@Parameter(property = "nsis.compression.lzma.dictsize")
	private int compressionDictSize = DEFAULT_LZMA_DICT_SIZE;

	/**
	 * Whether or not the compression defined in {@link #compression} is
	 * {@code FINAL}.
	 */
	@Parameter(property = "nsis.compression.final")
	private boolean compressionIsFinal;

	/**
	 * Whether or not the compression defined in {@link #compression} is
	 * {@code SOLID}.
	 */
	@Parameter(property = "nsis.compression.solid")
	private boolean compressionIsSolid;

	/**
	 * Indicates if the NSIS make operation should be disabled. If {@code true},
	 * no action will be taken when executing {@link MakeMojo}.
	 */
	@Parameter(property = "nsis.disabled", defaultValue = "false")
	private boolean disabled;

	/**
	 * A map of environment variables which will be passed to the execution of
	 * {@code makensis}.
	 */
	@Parameter
	private Map environmentVariables = new HashMap<>();

	/** The path of the generated header file. */
	@Parameter(property = "nsis.headerfile", defaultValue = "${project.build.directory}/project.nsh", required = true)
	private File headerFile;

	/**
	 * Whether or not {@link #headerFile} should be automatically injected as an
	 * {@code !include} in {@link #scriptFile}.
	 */
	@Parameter(property = "nsis.headerfile.inject", defaultValue = "true", required = true)
	private boolean injectHeaderFile;

	/**
	 * The folder to use as the working folder when running {@code makensis}.
	 * Relative paths will be resolved from this folder. By default this is the
	 * folder where {@link #scriptFile} is located.
	 */
	@Parameter(property = "nsis.makefolder")
	private String makeFolder;

	/**
	 * The path of the {@code makensis} executable to use. The default assumes
	 * that {@code makensis} can be found in the OS path.
	 */
	@Parameter(property = "nsis.makensis.executable", defaultValue = "makensis", required = true)
	private String makensisExecutable;

	/**
	 * The path of the {@code makensis} executable to use if the build platform
	 * is Linux. If {@code null} {@link #makensisExecutable} will be used also
	 * on Linux.
	 */
	@Parameter(property = "nsis.makensis.executable.linux")
	private String makensisExecutableLinux;

	/**
	 * The path of the {@code makensis} executable to use if the build platform
	 * is macOS. If {@code null} {@link #makensisExecutable} will be used also
	 * on macOS.
	 */
	@Parameter(property = "nsis.makensis.executable.macos")
	private String makensisExecutableMacOS;

	/**
	 * The value to use as {@code NSISDIR}. This will override
	 * {@link #autoNsisDir} and the environment variable named {@code NSISDIR}
	 * if set.
	 */
	@Parameter(property = "nsis.nsisdir")
	private String nsisDir;

	/** The path of the executable file to build. */
	@Parameter(property = "nsis.output.file", defaultValue = "${project.build.finalName}.exe", required = true)
	private String outputFile;

	/** The path of the NSIS script file to compile. */
	@Parameter(property = "nsis.scriptfile", defaultValue = "setup.nsi", required = true)
	private String scriptFile;

	/** The verbosity level to pass to {@code makensis}. */
	@Parameter(property = "nsis.verbosity", defaultValue = "2", required = true)
	private int verbosityLevel;

	/** The Maven project itself. */
	@Parameter(defaultValue = "${project}", required = true, readonly = true)
	private MavenProject project;

	/** Internal project helper component. */
	@Component
	private MavenProjectHelper projectHelper;

	/** For internal use */
	private Path outputFilePath;

	@Override
	public void execute() throws MojoExecutionException {
		if (disabled) {
			getLog().info("NSIS make: plugin is disabled, not doing anything");
			return;
		}

		validate();

		List arguments = commandBuilder();
		ProcessBuilder builder = new ProcessBuilder(arguments);
		// Set the working directory
		if (makeFolder == null) {
			builder.directory(project.getBasedir());
		} else {
			builder.directory(new File(makeFolder));
		}
		builder.redirectErrorStream(true);

		if (!isBlank(nsisDir)) {
			if (environmentVariables == null) {
				environmentVariables = new HashMap<>();
			}
			environmentVariables.put("NSISDIR", nsisDir);
		}

		if (environmentVariables != null) {
			builder.environment().putAll(environmentVariables);
		}

		if (getLog().isDebugEnabled()) {
			getLog().debug("directory:  " + builder.directory().getAbsolutePath());
			getLog().debug("commands  " + builder.command().toString());
			if (builder.environment() != null) {
				getLog().debug("environment variables: ");
				for (Map.Entry entry : builder.environment().entrySet()) {
					getLog().debug("  " + entry.getKey() + ": " + entry.getValue());
				}
			}
		}

		Charset charset = IS_WINDOWS ? Charset.defaultCharset() : StandardCharsets.UTF_8;
		try {
			StringBuilder sb = new StringBuilder("Executing:");
			for (String part : arguments) {
				sb.append(' ').append(part);
			}
			getLog().info(sb.toString());
			long start = System.currentTimeMillis();
			Process process = builder.start();
			ProcessOutputHandler output = new ProcessOutputHandler(process.getInputStream(), this, charset);
			output.startThread();

			int status;
			try {
				status = process.waitFor();
			} catch (InterruptedException e) {
				getLog().error("Makensis execution was interrupted before it could finish");
				process.destroy();
				status = -1;
			}

			if (status != 0) {
				throw new MojoExecutionException("Execution of makensis compiler failed. See output above for details.");
			}

			long end = System.currentTimeMillis();

			consumeOutputLine("Execution completed in " + (end - start) + "ms");

			if (attachArtifact) {
				// Attach the exe to the install tasks.
				if (outputFilePath == null) {
					getLog().warn("Could not attach output file because outputFilePath isn't set");
				} else {
					projectHelper.attachArtifact(project, "exe", classifier, outputFilePath.toFile());
				}
			}
		} catch (IOException e) {
			throw new MojoExecutionException("Unable to execute makensis", e);
		}
	}

	@Override
	public void consumeOutputLine(String line) {
		getLog().info("[MAKENSIS] " + line);
	}

	private void validate() throws MojoExecutionException {
		// Use the correct makensis binary
		if (OSTYPE == OSType.LINUX && !isBlank(makensisExecutableLinux)) {
			makensisExecutable = makensisExecutableLinux;
		} else if (OSTYPE == OSType.MACOS && !isBlank(makensisExecutableMacOS)) {
			makensisExecutable = makensisExecutableMacOS;
		}

		// Convert path separators
		try {
			Path path = Paths.get(makensisExecutable);
			boolean exists = Files.exists(path);
			if (exists) {
				path = path.toRealPath();
				makensisExecutable = path.toString();
			}
			if (autoNsisDir && isBlank(nsisDir)) {
				if (exists) {
					path = path.getParent();
				} else if (!path.isAbsolute()) {
					path = findInOSPath(path, getLog());
					if (path != null) {
						path = path.getParent();
					}
				} else {
					path = null;
				}
				if (path != null) {
					path = path.toRealPath();
					Path fileName = path.getFileName();
					if (fileName != null && "bin".equals(fileName.toString().toLowerCase(Locale.ROOT))) {
						path = path.getParent();
					}

					if (path != null) {
						// `/usr/local/bin` installations or macOS Homebrew keg-only
						Path shareNsis = path.resolve("share/nsis");
						if (Files.exists(shareNsis)) {
							path = shareNsis;
						}
					}
				}
				nsisDir = path == null ? null : path.toString();
			} else {
				path = Paths.get(nsisDir);
				nsisDir = path.toRealPath().toString();
			}
			if (isBlank(makeFolder)) {
				makeFolder = null;
			} else {
				path = Paths.get(makeFolder);
				makeFolder = path.toRealPath().toString();
			}
			if (isBlank(outputFile)) {
				outputFile = null;
			} else {
				path = Paths.get(outputFile);
				outputFile = path.toString();
			}
			path = Paths.get(scriptFile);
			scriptFile = path.toRealPath().toString();
		} catch (IOException e) {
			throw new MojoExecutionException("An error occurred while resolving paths: " + e.getMessage(), e);
		}

		// Check if the script file contains properties that conflict with the plugin configuration
		boolean finalCompression = compression != null && compressionIsFinal;
		boolean outputFileSet = outputFile != null;
		if (finalCompression || outputFileSet) {
			Pattern compressionPattern = finalCompression ? Pattern.compile("^\\s*SetCompressor\\s") : null;
			Pattern outputPattern = outputFileSet ? Pattern.compile("^\\s*OutFile\\s") : null;
			try (
				BufferedReader reader = new BufferedReader(
					new InputStreamReader(new FileInputStream(scriptFile), StandardCharsets.UTF_8)
				)
			) {
				int lineNo = 1;
				for (String line = reader.readLine(); line != null; line = reader.readLine()) {
					if (compressionPattern != null && compressionPattern.matcher(line).find()) {
						throw new MojoExecutionException(
							"Final compression is set in the plugin configuration but is also " +
							"defined in script file \"" + scriptFile + "\" on line " + lineNo + "."
						);
					}
					if (outputPattern != null && outputPattern.matcher(line).find()) {
						throw new MojoExecutionException(
							"The output file is set in the plugin configuration but is also " +
							"defined in script file \"" + scriptFile + "\" on line " + lineNo + "."
						);
					}
					lineNo++;
				}
			} catch (IOException e) {
				throw new MojoExecutionException(
					"An error occurred while validating script file \"" + scriptFile + "\": " + e.getMessage(),
					e
				);
			}
		}
	}

	/**
	 * Builds the command to use when launching {@code makensis}.
	 *
	 * @return The command {@link List}.
	 * @throws MojoExecutionException If an error occurs during the operation.
	 */
	@Nonnull
	protected List commandBuilder() throws MojoExecutionException {
		List result = new ArrayList<>();

		// Makensis binary
		result.add(makensisExecutable);

		outputFilePath = getOutputFile();
		String optionPrefix = IS_WINDOWS ? "/" : "-";

		// Include header file
		if (injectHeaderFile && headerFile.exists()) {
			result.add(optionPrefix + "X!include " + formatStringArgument(headerFile.getAbsolutePath(), false));
		}

		// Installer output file
		if (outputFilePath != null) {
			result.add(optionPrefix + "XOutFile " + formatStringArgument(outputFilePath.toAbsolutePath(), false));
		}

		// Working folder
		if (makeFolder != null) {
			result.add(optionPrefix + "NOCD");
		}

		// Verbosity level
		if (verbosityLevel < 0) {
			verbosityLevel = 0;
		} else if (verbosityLevel > 4) {
			verbosityLevel = 4;
		}
		result.add(optionPrefix + "V" + verbosityLevel);

		// Compress
		if (compress != null && compress != DEFAULT_COMPRESS) {
			result.add(optionPrefix + "XSetCompress " + compress.name());
		}

		// Compression
		if (compress != CompressOption.off) {
			if (
				compression != null && (
					compression != DEFAULT_COMPRESSION || compressionIsFinal || compressionIsSolid
			)) {
				StringBuilder setCompressor = new StringBuilder(optionPrefix + "XSetCompressor");
				if (compressionIsFinal) {
					setCompressor.append(" /FINAL");
				}
				if (compressionIsSolid) {
					setCompressor.append(" /SOLID");
				}
				setCompressor.append(' ').append(compression.name());
				result.add(setCompressor.toString());

				if (compression == CompressionType.lzma && compressionDictSize != DEFAULT_LZMA_DICT_SIZE) {
					result.add(optionPrefix + "XSetCompressorDictSize " + compressionDictSize);
				}
			}
		}

		// Script file
		result.add(scriptFile);
		getLog().debug("Processing Script file: \"" + scriptFile + "\"");

		return result;
	}

	/**
	 * Resolves the {@code OutFile} {@link Path}. If the destination folder
	 * doesn't exist, it will be created.
	 *
	 * @return The resolved {@code OutFile} {@link Path}.
	 * @throws MojoExecutionException If an error occurs during the operation.
	 */
	@Nullable
	protected Path getOutputFile() throws MojoExecutionException {
		if (isBlank(outputFile)) {
			return null;
		}
		Path path = Paths.get(outputFile);
		Path baseFolder = path.isAbsolute() ? null : Paths.get(project.getBuild().getDirectory());
		Path outputFolder = path.getParent();
		if (outputFolder == null) {
			outputFolder = baseFolder;
		} else if (baseFolder != null) {
			outputFolder = baseFolder.resolve(outputFolder);
		}

		// Make sure the output folder exists
		if (outputFolder != null && !Files.exists(outputFolder)) {
			try {
				Files.createDirectories(outputFolder);
			} catch (IOException e) {
				throw new MojoExecutionException(
					"Couldn't create target folder \"" + outputFolder.toAbsolutePath() + "\": " + e.getMessage(),
					e
				);
			}
		}

		if (classifier == null || classifier.isEmpty()) {
			return baseFolder == null ? path : baseFolder.resolve(path);
		}
		String classifierWithHyphenPrefix = classifier;
		if (classifier.charAt(0) != '-') {
			classifierWithHyphenPrefix = "-" + classifier;
		}
		int dot = outputFile.lastIndexOf('.');
		outputFile = dot >= 0 ?
			outputFile.substring(0, dot) + classifierWithHyphenPrefix + outputFile.substring(dot) :
			outputFile + classifierWithHyphenPrefix;
		return baseFolder == null ? Paths.get(outputFile) : baseFolder.resolve(outputFile);
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy