org.technologybrewery.habushu.exec.PoetryCommandHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of habushu-maven-plugin Show documentation
Show all versions of habushu-maven-plugin Show documentation
Leverages Poetry and Pyenv to provide an automated, predictable order of execution of build commands
that apply DevOps and configuration management best practices
The newest version!
package org.technologybrewery.habushu.exec;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.maven.plugin.MojoExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Facilitates the execution of Poetry commands.
*/
public class PoetryCommandHelper {
private static final String POETRY_COMMAND = "poetry";
private static final Logger logger = LoggerFactory.getLogger(PoetryCommandHelper.class);
private static final String extractVersionRegex = "[^0-9\\.]";
private File workingDirectory;
public PoetryCommandHelper(File workingDirectory) {
this.workingDirectory = workingDirectory;
}
/**
* Returns a {@link Boolean} and {@link String} {@link Pair} indicating whether
* Poetry is installed and if so, the version of Poetry that is installed. If
* Poetry is not installed, the returned {@link String} part of the {@link Pair}
* will be {@code null}.
*
* @return
*/
public Pair getIsPoetryInstalledAndVersion() {
try {
ProcessExecutor executor = createPoetryExecutor(Arrays.asList("--version"));
String versionResult = executor.executeAndGetResult(logger);
// Extracts version number from output, whether it's "Poetry version 1.1.15" or
// "Poetry (version 1.2.1)"
String version = versionResult.replaceAll(extractVersionRegex, "");
return new ImmutablePair(true, version);
} catch (Throwable e) {
return new ImmutablePair(false, null);
}
}
/**
* Returns a {@link String} indicating the relative path to the poetry
* cache directory. This is equivalent to {@code poetry config cache-dir}.
*
* @return
*/
public String getPoetryCacheDirectoryPath() throws MojoExecutionException {
return execute(Arrays.asList("config", "cache-dir"));
}
/**
* Returns whether the specified dependency package is installed within this
* Poetry project's virtual environment (and pyproject.toml).
*
* @param packageName
* @return
*/
public boolean isDependencyInstalled(String packageName) {
try {
execute(Arrays.asList("show", packageName));
} catch (Throwable e) {
return false;
}
return true;
}
/**
* Installs the specified package as a development dependency to this Poetry
* project's virtual environment and pyproject.toml specification.
*
* @param packageName
*/
public void installDevelopmentDependency(String packageName) throws MojoExecutionException {
execute(Arrays.asList("add", packageName, "--group", "dev"));
}
/**
* Executes a Poetry command with the given arguments, logs the executed
* command, and returns the resultant process output as a string. This method
* should be utilized when performing downstream logic based on the output of a
* Poetry command, or it is desirable to not show the command's generated
* stdout.
*
* @param arguments
* @return
* @throws MojoExecutionException
*/
public String execute(List arguments) throws MojoExecutionException {
if (logger.isInfoEnabled()) {
logger.info("Executing Poetry command: {} {}", POETRY_COMMAND, StringUtils.join(arguments, " "));
}
ProcessExecutor executor = createPoetryExecutor(arguments);
return executor.executeAndGetResult(logger);
}
/**
* Executes a Poetry command with the given arguments, logs the executed
* command, logs the stdout/stderr generated by the process, and returns the
* process exit code. This method should be utilized when it is desirable to
* immediately show all of the stdout/stderr produced by a Poetry command for
* diagnostic purposes.
*
* @param arguments
* @return
* @throws MojoExecutionException
*/
public int executeAndLogOutput(List arguments) throws MojoExecutionException {
if (logger.isInfoEnabled()) {
logger.info("Executing Poetry command: {} {}", POETRY_COMMAND, StringUtils.join(arguments, " "));
}
ProcessExecutor executor = createPoetryExecutor(arguments);
return executor.executeAndRedirectOutput(logger);
}
/**
* Executes a Poetry command with the given arguments and environment variables,
* logs the executed command and logs the stdout/stderr generated by the process.
* This method should be utilized when environment variables are needed for the poetry command,
* and it is desirable to immediately show all the stdout/stderr produced by a Poetry command for
* diagnostic purposes.
*
* @param arguments
* @param environmentVariables
*/
public void executeAndLogOutput(List arguments, Map environmentVariables) {
if (logger.isInfoEnabled()) {
logger.info("Executing Poetry command: {} {}", POETRY_COMMAND, StringUtils.join(arguments, " "));
}
ProcessExecutor executor = createPoetryExecutor(arguments, environmentVariables);
executor.executeAndRedirectOutput(logger);
}
/**
* Similar to {@link #executeAndLogOutput(List)}, except the executed Poetry
* command that is logged obfuscates/masks any given command arguments that are
* marked as sensitive. This method should be utilized if any Poetry command
* line arguments contain sensitive values that are not desirable to log, such
* as passwords.
*
* @param argAndIsSensitivePairs
* @return
* @throws MojoExecutionException
*/
public int executeWithSensitiveArgsAndLogOutput(List> argAndIsSensitivePairs)
throws MojoExecutionException {
if (logger.isInfoEnabled()) {
List argsWithSensitiveArgsMasked = argAndIsSensitivePairs.stream()
.map(pair -> pair.getRight() ? "XXXX" : pair.getLeft()).collect(Collectors.toList());
logger.info("Executing Poetry command: {} {}", POETRY_COMMAND,
StringUtils.join(argsWithSensitiveArgsMasked, " "));
}
ProcessExecutor executor = createPoetryExecutor(
argAndIsSensitivePairs.stream().map(Pair::getLeft).collect(Collectors.toList()));
return executor.executeAndRedirectOutput(logger);
}
/**
* Executes a Poetry command with the given arguments and logs a warning message
* if the command has not yet completed after the specified timeout period. This
* may be useful for providing input to developers when certain Poetry commands
* are running for longer than expected and may need to be manually halted due
* to cache-related issues.
* NOTE:The executed Poetry command will *not* be halted nor terminated
* when the timeout expires. After the timeout expires, this method will
* continue to wait until underlying Poetry command completes.
*
* @param arguments
* @param timeout
* @param timeUnit
* @return
*/
public Integer executePoetryCommandAndLogAfterTimeout(List arguments, int timeout, TimeUnit timeUnit) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future future = executor.submit(() -> this.executeAndLogOutput(arguments));
try {
return future.get(timeout, timeUnit);
} catch (TimeoutException e) {
logger.warn("poetry " + String.join(" ", arguments)
+ " has been running for quite some time, you may want to quit the mvn process (Ctrl+c) and run \"poetry cache clear . --all\" and restart your build.");
try {
return future.get();
} catch (InterruptedException | ExecutionException e1) {
throw new RuntimeException("Error occurred while waiting for Poetry command to complete", e1);
}
} catch (Exception e) {
throw new RuntimeException(String.format("Error occurred while performing Poetry command: poetry %s",
StringUtils.join(arguments, " ")), e);
} finally {
executor.shutdown();
}
}
/**
* Installs a Poetry plugin with the given name.
*
* @param name
* @return
* @throws MojoExecutionException
*/
public int installPoetryPlugin(String name) throws MojoExecutionException {
List args = new ArrayList();
args.add("self");
args.add("add");
args.add(name);
return this.executeAndLogOutput(args);
}
protected ProcessExecutor createPoetryExecutor(List arguments) {
List fullCommandArgs = new ArrayList<>();
fullCommandArgs.add(POETRY_COMMAND);
fullCommandArgs.addAll(arguments);
return new ProcessExecutor(workingDirectory, fullCommandArgs, Platform.guess(), null);
}
protected ProcessExecutor createPoetryExecutor(List arguments, Map environmentVariables) {
List fullCommandArgs = new ArrayList<>();
fullCommandArgs.add(POETRY_COMMAND);
fullCommandArgs.addAll(arguments);
return new ProcessExecutor(workingDirectory, fullCommandArgs, Platform.guess(), environmentVariables);
}
}