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
package org.technologybrewery.habushu.exec;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
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 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);
}
/**
* 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);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy