Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

Version 1 Next »

Version-Expression Transformations in Maven 2.2+

Relevant JIRAs

Background

In Maven 2.0.x, POMs that used an expression when specifying a version in a dependency, plugin, or the project itself (or its parent) resulted in those expressions being preserved in the POM that was installed or deployed. This leads to a variety of problems, the most common of which seems to arise when the expression resolves to a value derived from either the command line or the larger build environment (os.name for instance, or user.name). In order to make these POMs truly reflect the build environment from which they were deployed - and also to make them less apt to change in unpredictable ways based on the user's environment - version expressions needed to be resolved in any POM that gets installed or deployed.

This has been a particular problem for version elements, but in principle it's possible that it could be a problem for any element that forms part of an artifact coordinate, including groupId, artifactId, version, type/packaging, and classifier.

Initial Solution

Our initial attempt at solving this problem was to introduce a new ArtifactTransformation that runs each time an artifact is installed or deployed into a repository. The new transformation is managed - alongside things like the SnapshotTransformation - from the ArtifactTransformationManager, and is therefore invisible to basically everything outside of maven-artifact/maven-artifact-manager. This new ArtifactTransformation implementation (called VersionExpressionTransformation) performs a tightly focused interpolation step on the POM that's attached to an artifact via a ProjectArtifactMetadata instance. The first version of this transformation, released in Maven 2.1.0, simply did a string search for <version>*</version>, interpolated the element value, and replaced the original value in the POM content. Finally, the modified POM was written to <code>${project.build.directory}/pom-transformed.xml</code> and the new file replaced the old in the ProjectArtifactMetadata instance so it would be picked up during artifact installation or deployment.

Because of MNG-4140 (where <version/> elements in plugin configurations were being interpolated inappropriately in places such as parent POMs), the above approach has been revised to use targeted XPath expressions to isolate the element values to interpolate. The essential strategy remains the same, however.

Current Problem

In spite of the aforementioned problems that are solved by the VersionExpressionTransformation, this strategy of modifying POMs on install or deploy has introduced a new bug. Any plugin that produces an artifact or artifact metadata that is derived from the POM will be based on the unmodified file which hasn't had its version expressions interpolated. In cases like the shade plugin (which modifies the dependency specification of the POM) or the gpg plugin (which produces a signature of the unmodified POM), this will cause the plugin to produce incorrect metadata. Note that the shade plugin is reported as an issue here, but I'm personally not convinced that it is, since the shade plugin should be modifying the POM to reduce the dependencies list or modify their scope...which should probably happen ahead of any eventual dependency interpolation (or, at least, it shouldn't really matter which comes first).

Some Alternative Solutions

In order to accommodate plugins that need an accurate POM file before they execute, it's important that we take one of three approaches:

Plugin-Contributed ArtifactTransformations.

One possibility is to provide a mechanism by which plugins could contribute their own ArtifactTransformation implementations, and guarantee the order of operations for ArtifactTransformations. This would allow the GPG plugin to introduce its own signing transformation, which could attach the signature to the artifact after the version-expressions have been transformed by a previous ArtifactTransformer.

IMO, this approach has a couple serious problems. Specifically, it would complicate the classloading for any such plugins, forcing them to either declare themselves with <extensions>true</extensions>, or else forcing the ArtifactTransformationManager to somehow consume the classpath of any transformations that are added to it, including those jars in a classloader that could outlive the plugin that contributed it...which in turn means configuration and the component lifecycle of such a transformation could be complicated to maintain.

The other major problem with this strategy is that it puts the burden on the plugin developer to understand and manipulate the core build process in Maven. Not only would the GPG plugin have to know how to sign the POM file, but it would have to know about the artifact-transformation process in order to introduce a component to actually execute the plugin. IMO, this puts far too much responsibility on the plugin developer to know the intimate details of Maven's core, but it also violates the assumptions that a plugin developer has about Maven delivering accurate information for it to read of manipulate. In addition, it's also important to remember that Maven 3 is coming down the line, and we need to think twice before introducing new behaviors into plugins that will eventually have to make the transition to the new Maven. Artifact resolution in particular has been a hotspot for discussion related to this switch, so we need to maintain a stable API and set of behaviors in the 2.x code to help interoperability.

Pre-Process All POMs.

Another way we could solve this issue is to pre-process all POMs, interpolating version expressions and writing a modified POM to the target directory for subsequent use in the build process. The project representation in memory would be unaltered except for the MavenProject.file variable pointing to the modified POM instead of the original, the artifact installation/deployment process would be unaltered, and most plugins would be unaffected.

However, any plugin that reads the POM directly for whatever reason (often this is the most reliable way to get at the original, unmodified POM content) would read the original file that still contains the version expressions, which is different from the file that the entire rest of the build uses. As a workaround, it might be possible to add something like a <code>readRawPOM( File )</code> method to the MavenProjectBuilder component, to allow plugins to read the modified POM seamlessly instead of the original.

IMO, this isn't much of a solution. First, it means that the core build process in Maven - even without a single plugin executing - would modify the project directory by generating a modified POM file. Whether the POM even contains version expressions or not, whether the build needed the modified POM (the clean lifecycle likely wouldn't), this file would be generated. In many cases, this would amount to a performance penalty for IDEs and the like without providing any benefit. Finally, we will still have to re-release any plugin that might get in trouble by reading the POM file directly, in order to take advantage of the newly generated POM file. This has consequences for compatibility, both forward and backward, since Maven < 2.2.0 doesn't provide this file, and (presumably) neither will Maven 3.0.

My Proposed Solution

An alternative to the above, and the approach I much prefer, is to inject a new plugin into the default lifecycle mappings to do the POM transformation during the package phase. Unfortunately, the only manipulation of runtime build files/information that Maven does outside of executing plugins happens before the first plugin is executed. Then, each plugin is executed sequentially with only minimal core-level code running in between. The problem here is that it's no simple matter to inject core-level behavior at the package phase of the build lifecycle. In order to minimize the impact to core components like the LifecycleExecutor while still providing an accurate POM to plugins like GPG that typically execute relatively late in the lifecycle, the simplest solution is to use a plugin.

The POM transformation plugin should probably be embedded in the Maven core if possible, and bound into every lifecycle. The binding can be done in the lifecycle-mapping components - which means that any custom lifecycle mapping would have to be modified to include it - or it could be injected just before the lifecycle is executed. IMO, the best-case scenario would allow the plugin manager to load this new plugin from the core classloader, allowing it to be embedded directly in Maven itself. Finally, it would be good if possible to allow POMs to override the execution of this plugin, specifying a skip flag to turn it off, or even changing the <phase/> element to remap it into an earlier lifecycle phase if necessary.

  • No labels