org.technologybrewery.habushu.AbstractHabushuMojo Maven / Gradle / Ivy
Show all versions of habushu-maven-plugin Show documentation
package org.technologybrewery.habushu;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Server;
import org.apache.maven.settings.Settings;
import org.sonatype.plexus.components.cipher.PlexusCipherException;
import org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException;
import org.technologybrewery.habushu.exec.PoetryCommandHelper;
import org.technologybrewery.habushu.exec.PyenvCommandHelper;
import org.technologybrewery.habushu.util.MavenPasswordDecoder;
/**
* Contains logic common across the various Habushu mojos.
*/
public abstract class AbstractHabushuMojo extends AbstractMojo {
protected static final String SNAPSHOT = "-SNAPSHOT";
protected static final Pattern SEMVER2_PATTERN = Pattern.compile("\\d+\\.\\d+\\.\\d+-(rc|alpha|beta)\\.\\d+$",
Pattern.CASE_INSENSITIVE);
/**
* The current Maven user's settings, pulled dynamically from their settings.xml
* file.
*/
@Parameter(defaultValue = "${settings}", readonly = true, required = true)
protected Settings settings;
/**
* Toggle for whether the server password should be decrypted or retrieved as
* plain text.
*
* true (default) -> decrypt false -> plain text
*/
@Parameter(property = "habushu.decryptPassword", defaultValue = "true")
protected boolean decryptPassword;
/**
* The packaging type of the current Maven project. If it is not "habushu", then habushu packaging-related mojos
* will skip execution.
*/
@Parameter(defaultValue = "${project.packaging}", readonly = true, required = true)
protected String packaging;
/**
* Folder in which Python source files are located - should align with Poetry's
* project structure conventions.
*/
@Parameter(property = "habushu.sourceDirectory", required = true, defaultValue = "${project.basedir}/src")
protected File sourceDirectory;
/**
* Folder in which Python test files are located - should align with Poetry's
* project structure conventions.
*/
@Parameter(property = "habushu.testDirectory", required = true, defaultValue = "${project.basedir}/tests")
protected File testDirectory;
/**
* Specifies the {@code } of the {@code } element declared within
* the utilized settings.xml configuration that represents the desired
* credentials to use when publishing the package to the official public PyPI
* repository.
*/
protected static final String PUBLIC_PYPI_REPO_ID = "pypi";
/**
* Specifies the {@code } of the {@code } element declared within
* the utilized settings.xml configuration that represents the desired
* credentials to use when publishing the package to a dev PyPI repository.
*/
public static final String DEV_PYPI_REPO_ID = "dev-pypi";
/**
* Specifies the default dev pypi url to leverage.
*/
public static final String TEST_PYPI_REPOSITORY_URL = "https://test.pypi.org/";
/**
* Specifies the {@code } of the {@code } element declared within
* the utilized settings.xml configuration that represents the PyPI repository
* to which this project's archives will be published and/or used as a supplemental
* repository from which dependencies may be installed. This property is
* REQUIRED if publishing to or consuming dependencies from a private
* PyPI repository that requires authentication - it is expected that the
* relevant {@code } element provides the needed authentication details.
* If this property is *not* specified, this property will default to
* {@link #PUBLIC_PYPI_REPO_ID} and the execution of the {@code deploy}
* lifecycle phase will publish this package to the official public PyPI
* repository. Downstream package publishing functionality (i.e.
* {@link PublishToPyPiRepoMojo}) will use the relevant settings.xml
* {@code } declaration with a matching {@code } as credentials for
* publishing the package to the official public PyPI repository.
*/
@Parameter(property = "habushu.pypiRepoId", defaultValue = PUBLIC_PYPI_REPO_ID)
protected String pypiRepoId;
/**
* Specifies the URL of the private PyPI repository to which this project's
* archives will be published and/or used as a supplemental repository from which
* dependencies may be installed. This property is REQUIRED if publishing
* to or consuming dependencies from a private PyPI repository.
*/
@Parameter(property = "habushu.pypiRepoUrl")
protected String pypiRepoUrl;
/**
* Instructs deployment to use a development repository rather than a release repository. This is conceptually
* similar to Maven's release vs. snapshot repositories, allowing the release repository to only have formal
* releases with a separate repository for all 'dev' releases. Works in conjunction with the
* {@code devRepositoryId} and {@code devRepositoryUrl>} properties.
*/
@Parameter(property = "habushu.useDevRepository", defaultValue = "false")
protected boolean useDevRepository;
/**
* Specifies the {@code } of the {@code } element declared within
* the utilized settings.xml configuration that represents the PyPI dev repository
* to which this project's archives will be published and/or used as a supplemental
* repository from which dependencies may be installed when {@code useDevRepository}
* is enabled. This property is REQUIRED if publishing to or consuming
* dependencies from a devPyPI repository that requires authentication - it is
* expected that the relevant {@code } element provides the needed
* authentication details. If this property is *not* specified, this property will
* default to {@link #DEV_PYPI_REPO_ID} and the execution of the {@code deploy}
* lifecycle phase will publish this package to the official public Test PyPI
* repository. Downstream package publishing functionality (i.e.
* {@link PublishToPyPiRepoMojo}) will use the relevant settings.xml
* {@code } declaration with a matching {@code } as credentials for
* publishing the package to the official public test PyPI repository.
*/
@Parameter(property = "habushu.devRepositoryId", defaultValue = DEV_PYPI_REPO_ID)
protected String devRepositoryId;
/**
* Specifies the URL of the PyPI repository to which this project's dev
* archives will be published and/or used as a supplemental repository from which
* dependencies may be installed. This property is REQUIRED if publishing
* to or consuming dependencies from a private PyPI repository. Should end with a
* trailing "/".
*/
@Parameter(property = "habushu.devRepositoryUrl", defaultValue = TEST_PYPI_REPOSITORY_URL)
protected String devRepositoryUrl;
/**
* Specifies whether the version of the encapsulated Poetry package should be
* automatically managed and overridden where necessary by Habushu. If this
* property is true, Habushu may override the pyproject.toml defined version in
* the following build phases/mojos:
*
* - initialize ({@link InitializeHabushuMojo}): Automatically sets the
* Poetry package version to the version specified in the POM. If the POM is a
* SNAPSHOT, the Poetry package version will be set to the corresponding
* developmental release version without a numeric component (i.e. POM version
* of {@code 1.2.3-SNAPSHOT} will result in the Poetry package version being set
* to {@code 1.2.3.dev}). If the version is a release candidate (`rc`), `alpha`,
* or `beta` version in SemVer 2.0 format then it is translated to the equivalent
* PEP 440 format.
* - deploy ({@link PublishToPyPiRepoMojo}): Automatically sets the version of
* published Poetry packages that are SNAPSHOT modules to timestamped
* developmental release versions (i.e. POM version of {@code 1.2.3-SNAPSHOT}
* will result in the published Poetry package version to to
* {@code 1.2.3.dev1658238063}). After the package is published, the version of
* the SNAPSHOT module is reverted to its previous value (i.e.
* {@code 1.2.3.dev}).
*
* If {@link #overridePackageVersion} is set to false, none of the above
* automated version management operations will be performed.
*/
@Parameter(defaultValue = "true", property = "habushu.overridePackageVersion")
protected boolean overridePackageVersion;
/**
* Enables access to the runtime properties associated with the project's POM
* configuration against which Habushu is being executed.
*/
@Parameter(defaultValue = "${project}", readonly = true, required = true)
protected MavenProject project;
/**
* 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.
*/
@Parameter(defaultValue = "false", property = "habushu.rewriteLocalPathDepsInArchives")
protected boolean rewriteLocalPathDepsInArchives;
/**
* Find the username for a given server in Maven's user settings.
*
* @return the username for the server specified in Maven's settings.xml
*/
public String findUsernameForServer() {
return findUsernameForServer(this.pypiRepoId);
}
/**
* Find the username for a given server in Maven's user settings.
*
* @param repoId the id of the repository for which to find the username
* @return the username for the server specified in Maven's settings.xml
*/
public String findUsernameForServer(String repoId) {
Server server = this.settings.getServer(repoId);
return server != null ? server.getUsername() : null;
}
/**
* Find the password for a given server in Maven's user settings, decrypting password if needed.
*
* @return the password for the server specified in Maven's settings.xml
*/
public String findPasswordForServer() {
return findPasswordForServer(this.pypiRepoId);
}
/**
* Find the password for a given server in Maven's user settings, decrypting password if needed.
*
* @param repoId the id of the repository for which to find the password
* @return the password for the server specified in Maven's settings.xml
*/
public String findPasswordForServer(String repoId) {
String password = "";
if (this.decryptPassword) {
password = decryptServerPassword(repoId);
} else {
getLog().warn(
"Detected use of plain-text password! This is a security risk! Please consider using an encrypted password!");
password = findPlaintextPasswordForServer(repoId);
}
return password;
}
/**
* Simple utility method to decrypt a stored password for a server.
*
* @param repoId the ide of the repository for which to find the password
* @return the decrypted password for the server specified in Maven's settings.xml
*/
public String decryptServerPassword(String repoId) {
String decryptedPassword = null;
try {
decryptedPassword = MavenPasswordDecoder.decryptPasswordForServer(this.settings, repoId);
} catch (PlexusCipherException | SecDispatcherException e) {
throw new HabushuException("Unable to decrypt stored passwords.", e);
}
return decryptedPassword;
}
protected String findPlaintextPasswordForServer(String repoId) {
Server server = this.settings.getServer(repoId);
return server != null ? server.getPassword() : null;
}
/**
* Find the poetry cache directory.
*
* @return the poetry cache directory path as a FILE object.
*/
public File getCachedWheelDirectory(String artifactId) {
try {
PoetryCommandHelper poetryHelper = createPoetryCommandHelper();
String poetryCacheDirectoryPath = poetryHelper.getPoetryCacheDirectoryPath();
return new File(String.format("%s/cache/repositories/wheels/%s", poetryCacheDirectoryPath, artifactId));
} catch (Exception e) {
throw new HabushuException("Could not get the Poetry Cache Wheel directory!", e);
}
}
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if ("habushu".equals(packaging)) {
doExecute();
} else {
getLog().info("Skipping execution - packaging type is not 'habushu'");
}
}
protected abstract void doExecute() throws MojoExecutionException, MojoFailureException;
/**
* Gets the canonical path for a file without having to deal w/ checked
* exceptions.
*
* @param file file for which to get the canonical format
* @return canonical format
*/
protected String getCanonicalPathForFile(File file) {
try {
return file.getCanonicalPath();
} catch (IOException ioe) {
throw new HabushuException("Could not access file: " + file.getName(), ioe);
}
}
/**
* Creates a {@link PyenvCommandHelper} that may be used to invoke Pyenv
* commands from the project's working directory.
*
* @return
*/
protected PyenvCommandHelper createPyenvCommandHelper() {
return new PyenvCommandHelper(getPoetryProjectBaseDir());
}
/**
* Creates a {@link PoetryCommandHelper} that may be used to invoke Poetry
* commands from the project's working directory.
*
* @return
*/
protected PoetryCommandHelper createPoetryCommandHelper() {
return new PoetryCommandHelper(getPoetryProjectBaseDir());
}
/**
* Base directory in which Poetry projects will be located - should always be
* the basedir of the encapsulating Maven project.
*/
protected File getPoetryProjectBaseDir() {
return this.project.getBasedir();
}
/**
* Returns a {@link File} representing this project's Poetry pyproject.toml
* configuration.
*
* @return
*/
protected File getPoetryPyProjectTomlFile() {
return new File(getPoetryProjectBaseDir(), "pyproject.toml");
}
/**
* Gets the PEP-440 compliant Python package version associated with the given
* POM version.
*
* If the provided POM version is a SNAPSHOT, the version is converted into its
* corresponding developmental release version, with its numeric component
* optionally included based on the given {@code addSnapshotNumber} and
* {@code snapshotNumberDateFormatPattern} parameters. For example, given the
* POM version of {@code 1.2.3-SNAPSHOT}, a Python package version of
* {@code 1.2.3.dev} will be returned if {@code addSnapshotNumber} is false. If
* {@code addSnapshotNumber} is true, the numeric component will be added and
* defaults to the number of seconds from the epoch (i.e.
* {@code 1.2.3.dev1658238063}). The format of the snapshot number may be
* modified by providing a date format pattern (i.e. "YYYYMMddHHmm" would yield
* {@code 1.2.3.dev202207191002})
*
* If the provided POM version is a release version, it is expected to align
* with a valid PEP-440 final release version and is returned unmodified.
*
* @param pomVersion POM version of the encapsulating module in which Habushu is
* being executed.
* @return version number of the encapsulated Python package, appropriately
* formatted by the given parameters.
*/
protected static String getPythonPackageVersion(String pomVersion, boolean addSnapshotNumber,
String snapshotNumberDateFormatPattern) {
Matcher matcher = SEMVER2_PATTERN.matcher(pomVersion);
if(matcher.matches()) {
String qualifier = matcher.group(1);
pomVersion = pomVersion.replace("-" + qualifier + ".", qualifier);
}
String pythonPackageVersion = pomVersion;
if (isPomVersionSnapshot(pomVersion)) {
pythonPackageVersion = replaceSnapshotWithDev(pomVersion);
if (addSnapshotNumber) {
String snapshotNumber;
LocalDateTime currentTime = LocalDateTime.now();
if (StringUtils.isNotEmpty(snapshotNumberDateFormatPattern)) {
snapshotNumber = currentTime.format(DateTimeFormatter.ofPattern(snapshotNumberDateFormatPattern));
} else {
snapshotNumber = String.valueOf(currentTime.toEpochSecond(ZoneOffset.UTC));
}
pythonPackageVersion += snapshotNumber;
}
}
return pythonPackageVersion;
}
protected static String replaceSnapshotWithDev(String pomVersion) {
return pomVersion.substring(0, pomVersion.indexOf(SNAPSHOT)) + ".dev";
}
/**
* Returns whether the given POM version is a SNAPSHOT version.
*
* @param pomVersion
* @return
*/
protected static boolean isPomVersionSnapshot(String pomVersion) {
return pomVersion.endsWith(SNAPSHOT);
}
/**
* Finds and returns all custom tool poetry groups.
*
* @return list of custom tool poetry groups.
*/
protected List findCustomToolPoetryGroups() {
List toolPoetryGroupSections = new ArrayList<>();
File pyProjectTomlFile = getPoetryPyProjectTomlFile();
try (BufferedReader reader = new BufferedReader(new FileReader(pyProjectTomlFile))) {
String line = reader.readLine();
while (line != null) {
line = line.strip();
if (line.startsWith("[tool.poetry.group")) {
toolPoetryGroupSections.add(line.replace("[", StringUtils.EMPTY).replace("]", StringUtils.EMPTY));
}
line = reader.readLine();
}
} catch (IOException e) {
throw new HabushuException("Problem reading pyproject.toml to search for custom dependency groups!", e);
}
return toolPoetryGroupSections;
}
}