org.technologybrewery.habushu.PyenvAndPoetrySetup Maven / Gradle / Ivy
Show all versions of habushu-maven-plugin Show documentation
package org.technologybrewery.habushu;
import com.vdurmont.semver4j.Semver;
import com.vdurmont.semver4j.Semver.SemverType;
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.apache.maven.plugin.logging.Log;
import org.technologybrewery.habushu.exec.PoetryCommandHelper;
import org.technologybrewery.habushu.exec.PyenvCommandHelper;
import org.technologybrewery.habushu.exec.PythonVersionHelper;
import org.technologybrewery.habushu.util.PoetryUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Common class that ensures pre-requisite tools that Habushu leverages are installed and available on the
* developer's machine to support the same functionality across multiple Mojo implementations. These include:
*
* - pyenv
* - Poetry (installed version must satisfy {@link PoetryUtil#POETRY_VERSION_REQUIREMENT})
* - Required Poetry plugins (currently only {@code poetry-monorepo-dependency-plugin})
*
*/
public class PyenvAndPoetrySetup {
/**
* Specifies the semver compliant requirement for the default version of Python that
* must be installed and available for Habushu to use.
*/
static final String PYTHON_DEFAULT_VERSION_REQUIREMENT = "3.11.4";
private static final ThreadLocal validationStatusContainer = ThreadLocal.withInitial(ValidationTrackingStatus::new);
public static final String VALIDATED_IN_PRIOR_BUILD_PHASE = " (validated in prior build phase)";
/**
* The desired version of Python to use.
*/
protected String pythonVersion;
/**
* Should Habushu use pyenv to manage the utilized version of Python?
*/
protected boolean usePyenv;
/**
* Base directory from which to write poetry files.
*/
protected File baseDir;
/**
* Indicates whether Habushu should leverage the
* {@code poetry-monorepo-dependency-plugin} to rewrite any local path
* dependencies (to other Poetry projects) as versioned packaged dependencies in
* generated wheel/sdist archives. If {@code true}, Habushu will replace
* invocations of Poetry's {@code build} and {@code publish} commands in the
* {@link BuildDeploymentArtifactsMojo} and {@link PublishToPyPiRepoMojo} with
* the extensions of those commands exposed by the
* {@code poetry monorepo-dependency-plugin}, which are
* {@code build-rewrite-path-deps} and {@code publish-rewrite-path-deps}
* respectively.
*
* Typically, this flag will only be {@code true} when deploying/releasing
* Habushu modules within a CI environment that are part of a monorepo project
* structure which multiple Poetry projects depend on one another.
*/
protected boolean rewriteLocalPathDepsInArchives;
/**
* Logger from calling class to leverage.
*/
protected Log log;
/**
* File specifying the location of a generated shell script that will attempt to
* install the specified version of Python using "pyenv install --patch" with a
* patch that attempts to resolve the expected compilation error.
*/
private File patchInstallScript;
/**
* New instance - these values are typically passed in from Maven-enabled parameters in the calling Mojo.
*
* @param pythonVersion version of python to leverage
* @param usePyenv whether we are using pyenv to instance and activate python versions
* @param patchInstallScript patch install script path
* @param baseDir base directory from which to operate for this module
* @param rewriteLocalPathDepsInArchives see member variable for details
* @param log the logger to use for output
*/
public PyenvAndPoetrySetup(String pythonVersion, boolean usePyenv, File patchInstallScript,
File baseDir, boolean rewriteLocalPathDepsInArchives, Log log) {
this.pythonVersion = pythonVersion;
this.usePyenv = usePyenv;
this.patchInstallScript = patchInstallScript;
this.baseDir = baseDir;
this.rewriteLocalPathDepsInArchives = rewriteLocalPathDepsInArchives;
this.log = log;
}
public void execute() throws MojoExecutionException {
List missingRequiredToolMsgs = new ArrayList<>();
ValidationTrackingStatus validationTracker = validationStatusContainer.get();
String priorActivePythonVersionActivated = validationTracker.getPriorActivePythonVersionActivated();
if (pythonVersion.equals(priorActivePythonVersionActivated)) {
log.info("Using Python version: " + priorActivePythonVersionActivated + VALIDATED_IN_PRIOR_BUILD_PHASE);
} else {
String currentPythonVersion = "";
if (usePyenv) {
currentPythonVersion = validateAndConfigurePyenv(missingRequiredToolMsgs, currentPythonVersion);
} else {
currentPythonVersion = validateAndConfigureStraightPython();
}
// If a version of python is installed, verify that it matches the desired
// version
validatePythonVersion(currentPythonVersion);
validationTracker.setPriorActivePythonVersionActivated(currentPythonVersion);
}
PoetryCommandHelper poetryHelper = createPoetryCommandHelper();
String alreadyValidatedPoetryVersion = validationTracker.getAlreadyValidatedPoetryVersion();
if (!validationTracker.isAlreadyValidatedPoetryInstallation()) {
log.debug("Checking if Poetry is installed...");
Pair poetryInstallStatusAndVersion = poetryHelper.getIsPoetryInstalledAndVersion();
if (!poetryInstallStatusAndVersion.getLeft()) {
missingRequiredToolMsgs.add(
"'poetry' is not currently installed! Execute 'curl -sSL https://install.python-poetry.org | python -' to install or visit https://python-poetry.org/ for more information and installation options");
} else {
Semver poetryVersionSemver = new Semver(poetryInstallStatusAndVersion.getRight(), SemverType.NPM);
if (!poetryVersionSemver.satisfies(PoetryUtil.POETRY_VERSION_REQUIREMENT)) {
missingRequiredToolMsgs.add(String.format(
"Poetry version %s was installed - Habushu requires that installed version of Poetry satisfies %s. Please update Poetry by executing 'poetry self update' or visit https://python-poetry.org/docs/#installation for more information",
poetryInstallStatusAndVersion.getRight(), PoetryUtil.POETRY_VERSION_REQUIREMENT));
} else {
alreadyValidatedPoetryVersion = poetryInstallStatusAndVersion.getRight();
validationTracker.setAlreadyValidatedPoetryVersion(alreadyValidatedPoetryVersion);
validationTracker.setAlreadyValidatedPoetryInstallation(true);
}
}
} else {
alreadyValidatedPoetryVersion += VALIDATED_IN_PRIOR_BUILD_PHASE;
}
log.info("Found Poetry " + alreadyValidatedPoetryVersion);
if (!missingRequiredToolMsgs.isEmpty()) {
throw new MojoExecutionException(StringUtils.join(System.lineSeparator(), missingRequiredToolMsgs, System.lineSeparator()));
}
// check for existing poetry.toml, warn that this file should be tracked in version control if one does not exist
String poetryTomlPath = baseDir.getAbsolutePath() + "/poetry.toml";
File poetryToml = new File(poetryTomlPath);
if (!poetryToml.exists()) {
log.warn("Did not find a poetry.toml within the current project. It is recommended to always include this file in version control to ensure consistent builds.");
}
if (usePyenv) {
log.info("Configuring Poetry to use the pyenv-activated Python binary...");
poetryHelper.executeAndLogOutput(Arrays.asList("config", "--local", "virtualenvs.prefer-active-python", "true"));
}
}
void installPoetryMonorepoDependencyPlugin() throws MojoExecutionException {
PoetryCommandHelper poetryHelper = createPoetryCommandHelper();
log.info("Checking for updates to poetry-monorepo-dependency-plugin...");
poetryHelper.installPoetryPlugin("poetry-monorepo-dependency-plugin@latest");
}
void registerRepositoryToSupportAuthenticatedDependencyResolution(String repoId, String username, String password) throws MojoExecutionException {
PoetryCommandHelper poetryHelper = createPoetryCommandHelper();
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
log.info(String.format("Did not find username and password for the server with %s. Will use existing configuration.", repoId));
} else {
String configKey = String.format("http-basic.%s", repoId);
log.info(String.format("Adding username and password configuration for %s", repoId));
List> credentialConfigurationArgs = new ArrayList<>();
credentialConfigurationArgs.add(new ImmutablePair<>("config", false));
credentialConfigurationArgs.add(new ImmutablePair<>(configKey, false));
credentialConfigurationArgs.add(new ImmutablePair<>(username, false));
credentialConfigurationArgs.add(new ImmutablePair<>(password, true));
poetryHelper.executeWithSensitiveArgsAndLogOutput(credentialConfigurationArgs);
}
}
private void validatePythonVersion(String currentPythonVersion) throws MojoExecutionException {
if (StringUtils.isNotBlank(currentPythonVersion)) {
if (!currentPythonVersion.equals(pythonVersion)) {
throw new MojoExecutionException(String.format("Expected Python version %s, but found version %s",
pythonVersion, currentPythonVersion));
}
String sourceMessage = usePyenv ? "(managed by pyenv)" : "(managed by the operating system)";
log.info(String.format("Using Python %s %s", currentPythonVersion, sourceMessage));
}
}
private String validateAndConfigureStraightPython() throws MojoExecutionException {
String currentPythonVersion;
PythonVersionHelper pythonVersionHelper = new PythonVersionHelper(baseDir, pythonVersion);
try {
currentPythonVersion = pythonVersionHelper.getCurrentPythonVersion();
} catch (MojoExecutionException mojoExecutionException) {
throw new MojoExecutionException(
"Expected Python version " + pythonVersion + ", but it was not installed");
}
return currentPythonVersion;
}
private String validateAndConfigurePyenv(List missingRequiredToolMsgs, String currentPythonVersion) throws MojoExecutionException {
PyenvCommandHelper pyenvHelper = createPyenvCommandHelper();
log.debug("Checking if pyenv is installed...");
if (!pyenvHelper.isPyenvInstalled()) {
missingRequiredToolMsgs.add(
"'pyenv' is not currently installed! Please install pyenv and try again. Visit https://github.com/pyenv/pyenv for more information.");
} else {
currentPythonVersion = pyenvHelper.getCurrentPythonVersion();
if (!pythonVersion.equals(currentPythonVersion)) {
pyenvHelper.updatePythonVersion(pythonVersion, patchInstallScript);
currentPythonVersion = pyenvHelper.getCurrentPythonVersion();
}
// Check for misconfigured pyenv that looks right, but is actually not "taking" due to missing PATH setup:
PythonVersionHelper pythonVersionHelper = new PythonVersionHelper(baseDir, pythonVersion);
String postPyenvActivatedPythonVersion = pythonVersionHelper.getCurrentPythonVersion();
if (!pythonVersion.equals(postPyenvActivatedPythonVersion)) {
missingRequiredToolMsgs.add(String.format("Expected 'pyenv' to set Python to %s but instead found %s!",
pythonVersion, postPyenvActivatedPythonVersion));
missingRequiredToolMsgs.add("'pyenv' is installed, but not configured correctly. " +
"Ensure your PATH includes 'pyenv init -' expected content " +
"OR do not configure habushu to use 'pyenv' to manage the Python version!");
}
log.debug("pyenv already installed");
}
return currentPythonVersion;
}
/**
* Creates a {@link PyenvCommandHelper} that may be used to invoke Pyenv
* commands from the project's working directory.
*
* @return command helper
*/
protected PyenvCommandHelper createPyenvCommandHelper() {
return new PyenvCommandHelper(baseDir);
}
/**
* Creates a {@link PoetryCommandHelper} that may be used to invoke Poetry
* commands from the project's working directory.
*
* @return command helper
*/
protected PoetryCommandHelper createPoetryCommandHelper() {
return new PoetryCommandHelper(baseDir);
}
}