You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2023/01/25 10:27:05 UTC

[maven] branch master updated: [MNG-7622] Maven Transformation and Consumer POM (#907)

This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new 8918c8144 [MNG-7622] Maven Transformation and Consumer POM (#907)
8918c8144 is described below

commit 8918c8144f341fb9585025540c4571c0358ad4f8
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Wed Jan 25 11:27:00 2023 +0100

    [MNG-7622] Maven Transformation and Consumer POM (#907)
    
    Maven Consumer POM redone, it happens only in "maven3 realm" (before resolver), and allows use cases like m-gog-p and checksum-m-p work as before.
    
    Key aspects:
    * consumer POM is injected to build earliest possible as attached artifact
    * it lives and is visible just like any other attached artifact (so m-gpg-p can process it)
    * just before the install/deploy, they are "swapped out" to replace POM along with all "extras" it may have (checksum, signature)
    * to support use cases like MNG-7067 (in memory model is changed, but not POM file), OnChangeTransformer could be extended to take into account both: file content and model content.
    
    ---
    
    https://issues.apache.org/jira/browse/MNG-7622
---
 .../aether/ConsumerModelSourceTransformer.java     |  47 ------
 .../DefaultRepositorySystemSessionFactory.java     |  42 -----
 .../maven/internal/aether/MavenDeployer.java       |  58 +++++++
 .../maven/internal/aether/MavenInstaller.java      |  58 +++++++
 .../maven/internal/aether/ResolverLifecycle.java   |   4 +-
 .../ConsumerPomArtifactTransformer.java            | 169 +++++++++++++++++++++
 .../transformation/OnChangeTransformer.java        | 104 +++++++++++++
 .../transformation/TransformedArtifact.java        | 146 ++++++++++++++++++
 .../lifecycle/internal/LifecycleModuleBuilder.java |   6 +
 .../lifecycle/internal/builder/BuilderCommon.java  |  21 ---
 .../ConsumerPomArtifactTransformerTest.java}       |   9 +-
 .../internal/LifecycleModuleBuilderTest.java       |   4 +-
 .../java/org/apache/maven/feature/Features.java    |  20 ++-
 13 files changed, 568 insertions(+), 120 deletions(-)

diff --git a/maven-core/src/main/java/org/apache/maven/internal/aether/ConsumerModelSourceTransformer.java b/maven-core/src/main/java/org/apache/maven/internal/aether/ConsumerModelSourceTransformer.java
deleted file mode 100644
index c07dcc0c7..000000000
--- a/maven-core/src/main/java/org/apache/maven/internal/aether/ConsumerModelSourceTransformer.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.internal.aether;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-import org.apache.maven.model.building.DefaultBuildPomXMLFilterFactory;
-import org.apache.maven.model.building.TransformerContext;
-import org.apache.maven.model.transform.RawToConsumerPomXMLFilterFactory;
-import org.apache.maven.model.transform.pull.XmlUtils;
-import org.codehaus.plexus.util.ReaderFactory;
-import org.codehaus.plexus.util.xml.XmlStreamReader;
-import org.codehaus.plexus.util.xml.pull.EntityReplacementMap;
-import org.codehaus.plexus.util.xml.pull.MXParser;
-import org.codehaus.plexus.util.xml.pull.XmlPullParser;
-import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
-
-class ConsumerModelSourceTransformer {
-    public InputStream transform(Path pomFile, TransformerContext context) throws IOException, XmlPullParserException {
-        XmlStreamReader reader = ReaderFactory.newXmlReader(Files.newInputStream(pomFile));
-        XmlPullParser parser = new MXParser(EntityReplacementMap.defaultEntityReplacementMap);
-        parser.setInput(reader);
-        parser = new RawToConsumerPomXMLFilterFactory(new DefaultBuildPomXMLFilterFactory(context, true))
-                .get(parser, pomFile);
-
-        return XmlUtils.writeDocument(reader, parser);
-    }
-}
diff --git a/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java b/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
index ae4efb1e6..1f03faf11 100644
--- a/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
+++ b/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
@@ -21,13 +21,8 @@ package org.apache.maven.internal.aether;
 import javax.inject.Inject;
 import javax.inject.Named;
 
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -41,11 +36,9 @@ import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
 import org.apache.maven.bridge.MavenRepositorySystem;
 import org.apache.maven.eventspy.internal.EventSpyDispatcher;
 import org.apache.maven.execution.MavenExecutionRequest;
-import org.apache.maven.feature.Features;
 import org.apache.maven.internal.xml.XmlNodeImpl;
 import org.apache.maven.internal.xml.XmlPlexusConfiguration;
 import org.apache.maven.model.ModelBase;
-import org.apache.maven.model.building.TransformerContext;
 import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
 import org.apache.maven.rtinfo.RuntimeInformation;
 import org.apache.maven.settings.Mirror;
@@ -56,12 +49,9 @@ import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest;
 import org.apache.maven.settings.crypto.SettingsDecrypter;
 import org.apache.maven.settings.crypto.SettingsDecryptionResult;
 import org.codehaus.plexus.configuration.PlexusConfiguration;
-import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
 import org.eclipse.aether.ConfigurationProperties;
 import org.eclipse.aether.DefaultRepositorySystemSession;
 import org.eclipse.aether.RepositorySystem;
-import org.eclipse.aether.SessionData;
-import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.repository.LocalRepository;
 import org.eclipse.aether.repository.LocalRepositoryManager;
 import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
@@ -69,8 +59,6 @@ import org.eclipse.aether.repository.RepositoryPolicy;
 import org.eclipse.aether.repository.WorkspaceReader;
 import org.eclipse.aether.resolution.ResolutionErrorPolicy;
 import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
-import org.eclipse.aether.transform.FileTransformer;
-import org.eclipse.aether.transform.TransformException;
 import org.eclipse.aether.util.ConfigUtils;
 import org.eclipse.aether.util.listener.ChainedRepositoryListener;
 import org.eclipse.aether.util.repository.AuthenticationBuilder;
@@ -377,10 +365,6 @@ public class DefaultRepositorySystemSessionFactory {
 
         setUpLocalRepositoryManager(request, session);
 
-        if (Features.buildConsumer(request.getUserProperties()).isActive()) {
-            session.setFileTransformerManager(a -> getTransformersForArtifact(a, session.getData()));
-        }
-
         return session;
     }
 
@@ -437,30 +421,4 @@ public class DefaultRepositorySystemSessionFactory {
         return "Apache-Maven" + version + " (Java " + System.getProperty("java.version") + "; "
                 + System.getProperty("os.name") + " " + System.getProperty("os.version") + ")";
     }
-
-    private Collection<FileTransformer> getTransformersForArtifact(
-            final Artifact artifact, final SessionData sessionData) {
-        TransformerContext context = (TransformerContext) sessionData.get(TransformerContext.KEY);
-        Collection<FileTransformer> transformers = new ArrayList<>();
-
-        // In case of install:install-file there's no transformer context, as the goal is unrelated to the lifecycle.
-        if ("pom".equals(artifact.getExtension()) && context != null) {
-            transformers.add(new FileTransformer() {
-                @Override
-                public InputStream transformData(File pomFile) throws IOException, TransformException {
-                    try {
-                        return new ConsumerModelSourceTransformer().transform(pomFile.toPath(), context);
-                    } catch (XmlPullParserException e) {
-                        throw new TransformException(e);
-                    }
-                }
-
-                @Override
-                public Artifact transformArtifact(Artifact artifact) {
-                    return artifact;
-                }
-            });
-        }
-        return Collections.unmodifiableCollection(transformers);
-    }
 }
diff --git a/maven-core/src/main/java/org/apache/maven/internal/aether/MavenDeployer.java b/maven-core/src/main/java/org/apache/maven/internal/aether/MavenDeployer.java
new file mode 100644
index 000000000..93aa84418
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/internal/aether/MavenDeployer.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.internal.aether;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.apache.maven.internal.transformation.ConsumerPomArtifactTransformer;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.deployment.DeployRequest;
+import org.eclipse.aether.deployment.DeployResult;
+import org.eclipse.aether.deployment.DeploymentException;
+import org.eclipse.aether.impl.Deployer;
+import org.eclipse.aether.internal.impl.DefaultDeployer;
+import org.eclipse.sisu.Priority;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Maven specific deployer.
+ */
+@Singleton
+@Named
+@Priority(100)
+final class MavenDeployer implements Deployer {
+
+    private final DefaultDeployer deployer;
+
+    private final ConsumerPomArtifactTransformer consumerPomArtifactTransformer;
+
+    @Inject
+    MavenDeployer(DefaultDeployer deployer, ConsumerPomArtifactTransformer consumerPomArtifactTransformer) {
+        this.deployer = requireNonNull(deployer);
+        this.consumerPomArtifactTransformer = requireNonNull(consumerPomArtifactTransformer);
+    }
+
+    @Override
+    public DeployResult deploy(RepositorySystemSession session, DeployRequest request) throws DeploymentException {
+        return deployer.deploy(session, consumerPomArtifactTransformer.remapDeployArtifacts(session, request));
+    }
+}
diff --git a/maven-core/src/main/java/org/apache/maven/internal/aether/MavenInstaller.java b/maven-core/src/main/java/org/apache/maven/internal/aether/MavenInstaller.java
new file mode 100644
index 000000000..d7f2f95f1
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/internal/aether/MavenInstaller.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.internal.aether;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.apache.maven.internal.transformation.ConsumerPomArtifactTransformer;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.Installer;
+import org.eclipse.aether.installation.InstallRequest;
+import org.eclipse.aether.installation.InstallResult;
+import org.eclipse.aether.installation.InstallationException;
+import org.eclipse.aether.internal.impl.DefaultInstaller;
+import org.eclipse.sisu.Priority;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Maven specific installer.
+ */
+@Singleton
+@Named
+@Priority(100)
+final class MavenInstaller implements Installer {
+
+    private final DefaultInstaller installer;
+
+    private final ConsumerPomArtifactTransformer consumerPomArtifactTransformer;
+
+    @Inject
+    MavenInstaller(DefaultInstaller installer, ConsumerPomArtifactTransformer consumerPomArtifactTransformer) {
+        this.installer = requireNonNull(installer);
+        this.consumerPomArtifactTransformer = requireNonNull(consumerPomArtifactTransformer);
+    }
+
+    @Override
+    public InstallResult install(RepositorySystemSession session, InstallRequest request) throws InstallationException {
+        return installer.install(session, consumerPomArtifactTransformer.remapInstallArtifacts(session, request));
+    }
+}
diff --git a/maven-core/src/main/java/org/apache/maven/internal/aether/ResolverLifecycle.java b/maven-core/src/main/java/org/apache/maven/internal/aether/ResolverLifecycle.java
index 576e915a6..11b6da681 100644
--- a/maven-core/src/main/java/org/apache/maven/internal/aether/ResolverLifecycle.java
+++ b/maven-core/src/main/java/org/apache/maven/internal/aether/ResolverLifecycle.java
@@ -35,11 +35,11 @@ import static java.util.Objects.requireNonNull;
  */
 @Named
 @EagerSingleton
-public final class ResolverLifecycle {
+final class ResolverLifecycle {
     private final Provider<RepositorySystem> repositorySystemProvider;
 
     @Inject
-    public ResolverLifecycle(Provider<RepositorySystem> repositorySystemProvider) {
+    ResolverLifecycle(Provider<RepositorySystem> repositorySystemProvider) {
         this.repositorySystemProvider = requireNonNull(repositorySystemProvider);
     }
 
diff --git a/maven-core/src/main/java/org/apache/maven/internal/transformation/ConsumerPomArtifactTransformer.java b/maven-core/src/main/java/org/apache/maven/internal/transformation/ConsumerPomArtifactTransformer.java
new file mode 100644
index 000000000..1b6c5a8d0
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/internal/transformation/ConsumerPomArtifactTransformer.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.internal.transformation;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.function.BiConsumer;
+
+import org.apache.maven.feature.Features;
+import org.apache.maven.model.building.DefaultBuildPomXMLFilterFactory;
+import org.apache.maven.model.building.TransformerContext;
+import org.apache.maven.model.transform.RawToConsumerPomXMLFilterFactory;
+import org.apache.maven.model.transform.pull.XmlUtils;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.artifact.ProjectArtifact;
+import org.codehaus.plexus.util.ReaderFactory;
+import org.codehaus.plexus.util.xml.XmlStreamReader;
+import org.codehaus.plexus.util.xml.pull.EntityReplacementMap;
+import org.codehaus.plexus.util.xml.pull.MXParser;
+import org.codehaus.plexus.util.xml.pull.XmlPullParser;
+import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.deployment.DeployRequest;
+import org.eclipse.aether.installation.InstallRequest;
+
+/**
+ * Consumer POM transformer.
+ *
+ * @since TBD
+ */
+@Singleton
+@Named("consumer-pom")
+public final class ConsumerPomArtifactTransformer {
+
+    private static final String CONSUMER_POM_CLASSIFIER = "consumer";
+
+    public void injectTransformedArtifacts(MavenProject project, RepositorySystemSession session) throws IOException {
+        if (isActive(session)) {
+            Path generatedFile;
+            String buildDirectory =
+                    project.getBuild() != null ? project.getBuild().getDirectory() : null;
+            if (buildDirectory == null) {
+                generatedFile = Files.createTempFile(CONSUMER_POM_CLASSIFIER, "pom");
+            } else {
+                Path buildDir = Paths.get(buildDirectory);
+                Files.createDirectories(buildDir);
+                generatedFile = Files.createTempFile(buildDir, CONSUMER_POM_CLASSIFIER, "pom");
+            }
+            project.addAttachedArtifact(new ConsumerPomArtifact(project, generatedFile, session));
+        }
+    }
+
+    public InstallRequest remapInstallArtifacts(RepositorySystemSession session, InstallRequest request) {
+        if (isActive(session) && consumerPomPresent(request.getArtifacts())) {
+            request.setArtifacts(replacePom(request.getArtifacts()));
+        }
+        return request;
+    }
+
+    public DeployRequest remapDeployArtifacts(RepositorySystemSession session, DeployRequest request) {
+        if (isActive(session) && consumerPomPresent(request.getArtifacts())) {
+            request.setArtifacts(replacePom(request.getArtifacts()));
+        }
+        return request;
+    }
+
+    private boolean isActive(RepositorySystemSession session) {
+        return Features.buildConsumer(session.getUserProperties()).isActive();
+    }
+
+    private boolean consumerPomPresent(Collection<Artifact> artifacts) {
+        return artifacts.stream().anyMatch(a -> CONSUMER_POM_CLASSIFIER.equals(a.getClassifier()));
+    }
+
+    private Collection<Artifact> replacePom(Collection<Artifact> artifacts) {
+        ArrayList<Artifact> result = new ArrayList<>(artifacts.size());
+        for (Artifact artifact : artifacts) {
+            if (CONSUMER_POM_CLASSIFIER.equals(artifact.getClassifier())) {
+                // if under CONSUMER_POM_CLASSIFIER, move it to "" classifier
+                DefaultArtifact remapped = new DefaultArtifact(
+                        artifact.getGroupId(),
+                        artifact.getArtifactId(),
+                        "",
+                        artifact.getExtension(),
+                        artifact.getVersion(),
+                        artifact.getProperties(),
+                        artifact.getFile());
+                result.add(remapped);
+            } else if ("".equals(artifact.getClassifier())
+                            && (artifact.getExtension().equals("pom"))
+                    || artifact.getExtension().startsWith("pom.")) {
+                // skip POM and POM subordinates
+                continue;
+            } else {
+                // everything else: add as is
+                result.add(artifact);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Consumer POM is transformed from original POM.
+     */
+    private static class ConsumerPomArtifact extends TransformedArtifact {
+
+        private ConsumerPomArtifact(MavenProject mavenProject, Path target, RepositorySystemSession session) {
+            super(
+                    new ProjectArtifact(mavenProject),
+                    () -> mavenProject.getFile().toPath(),
+                    CONSUMER_POM_CLASSIFIER,
+                    "pom",
+                    target,
+                    transformer(session));
+        }
+
+        private static BiConsumer<Path, Path> transformer(RepositorySystemSession session) {
+            TransformerContext context = (TransformerContext) session.getData().get(TransformerContext.KEY);
+            return (src, dest) -> {
+                try (InputStream inputStream = transform(src, context)) {
+                    Files.createDirectories(dest.getParent());
+                    Files.copy(inputStream, dest, StandardCopyOption.REPLACE_EXISTING);
+                } catch (XmlPullParserException | IOException e) {
+                    throw new RuntimeException(e);
+                }
+            };
+        }
+    }
+
+    /**
+     * The actual transformation: visible for testing.
+     */
+    static InputStream transform(Path pomFile, TransformerContext context) throws IOException, XmlPullParserException {
+        XmlStreamReader reader = ReaderFactory.newXmlReader(Files.newInputStream(pomFile));
+        XmlPullParser parser = new MXParser(EntityReplacementMap.defaultEntityReplacementMap);
+        parser.setInput(reader);
+        parser = new RawToConsumerPomXMLFilterFactory(new DefaultBuildPomXMLFilterFactory(context, true))
+                .get(parser, pomFile);
+
+        return XmlUtils.writeDocument(reader, parser);
+    }
+}
diff --git a/maven-core/src/main/java/org/apache/maven/internal/transformation/OnChangeTransformer.java b/maven-core/src/main/java/org/apache/maven/internal/transformation/OnChangeTransformer.java
new file mode 100644
index 000000000..d777ed206
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/internal/transformation/OnChangeTransformer.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.internal.transformation;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Keeps transformed file up-to-date relative to its source file. It manages state (i.e. hashing the content) using
+ * passed in stateFunction, and transforms when needed using passed in transformer bi-consumer.
+ * <p>
+ * Covered cases:
+ * <ul>
+ *     <li>when source supplier returns {@code null}, this class will return {@code null}.</li>
+ *     <li>when source supplier returns non existing path, this class will return non existing path.</li>
+ *     <li>when source supplier returns existing path, this class will ensure transformation is in sync.</li>
+ * </ul>
+ *
+ * @since TBD
+ */
+final class OnChangeTransformer implements Supplier<Path> {
+
+    private final Supplier<Path> source;
+
+    private final Path target;
+
+    private final Function<Path, String> stateFunction;
+
+    private final BiConsumer<Path, Path> transformerConsumer;
+
+    private final AtomicReference<String> sourceState;
+
+    OnChangeTransformer(
+            Supplier<Path> source,
+            Path target,
+            Function<Path, String> stateFunction,
+            BiConsumer<Path, Path> transformerConsumer) {
+        this.source = requireNonNull(source);
+        this.target = requireNonNull(target);
+        this.stateFunction = requireNonNull(stateFunction);
+        this.transformerConsumer = requireNonNull(transformerConsumer);
+        this.sourceState = new AtomicReference<>(null);
+    }
+
+    @Override
+    public synchronized Path get() {
+        String state = mayUpdate();
+        if (state == null) {
+            return null;
+        }
+        return target;
+    }
+
+    private String mayUpdate() {
+        String result;
+        try {
+            Path src = source.get();
+            if (src == null) {
+                Files.deleteIfExists(target);
+                result = null;
+            } else if (!Files.exists(src)) {
+                Files.deleteIfExists(target);
+                result = "";
+            } else {
+                String current = stateFunction.apply(src);
+                String existing = sourceState.get();
+                if (!Objects.equals(current, existing)) {
+                    transformerConsumer.accept(src, target);
+                    Files.setLastModifiedTime(target, Files.getLastModifiedTime(src));
+                }
+                result = current;
+            }
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+        sourceState.set(result);
+        return result;
+    }
+}
diff --git a/maven-core/src/main/java/org/apache/maven/internal/transformation/TransformedArtifact.java b/maven-core/src/main/java/org/apache/maven/internal/transformation/TransformedArtifact.java
new file mode 100644
index 000000000..84e88c399
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/internal/transformation/TransformedArtifact.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.internal.transformation;
+
+import java.io.File;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.MessageDigest;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.DefaultArtifact;
+import org.apache.maven.artifact.handler.ArtifactHandler;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Transformed artifact is derived with some transformation from source artifact.
+ *
+ * @since TBD
+ */
+abstract class TransformedArtifact extends DefaultArtifact {
+
+    private final OnChangeTransformer onChangeTransformer;
+
+    TransformedArtifact(
+            Artifact source,
+            Supplier<Path> sourcePathProvider,
+            String classifier,
+            String extension,
+            Path targetPath,
+            BiConsumer<Path, Path> transformerConsumer) {
+        super(
+                source.getGroupId(),
+                source.getArtifactId(),
+                source.getVersionRange(),
+                source.getScope(),
+                extension,
+                classifier,
+                new TransformedArtifactHandler(
+                        classifier, extension, source.getArtifactHandler().getPackaging()));
+        this.onChangeTransformer =
+                new OnChangeTransformer(sourcePathProvider, targetPath, TransformedArtifact::sha1, transformerConsumer);
+    }
+
+    @Override
+    public boolean isResolved() {
+        return getFile() != null;
+    }
+
+    @Override
+    public void setFile(File file) {
+        throw new IllegalStateException("transformed artifact file cannot be set");
+    }
+
+    @Override
+    public File getFile() {
+        Path result = onChangeTransformer.get();
+        if (result == null) {
+            return null;
+        }
+        return result.toFile();
+    }
+
+    private static final int BUFFER_SIZE = 8192;
+
+    private static String sha1(Path path) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("SHA-1");
+            try (InputStream fis = Files.newInputStream(path)) {
+                byte[] buffer = new byte[BUFFER_SIZE];
+                int read;
+                while ((read = fis.read(buffer)) != -1) {
+                    md.update(buffer, 0, read);
+                }
+            }
+            StringBuilder result = new StringBuilder();
+            for (byte b : md.digest()) {
+                result.append(String.format("%02x", b));
+            }
+            return result.toString();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static class TransformedArtifactHandler implements ArtifactHandler {
+        private final String classifier;
+
+        private final String extension;
+
+        private final String packaging;
+
+        private TransformedArtifactHandler(String classifier, String extension, String packaging) {
+            this.classifier = classifier;
+            this.extension = requireNonNull(extension);
+            this.packaging = requireNonNull(packaging);
+        }
+
+        public String getClassifier() {
+            return classifier;
+        }
+
+        public String getDirectory() {
+            return null;
+        }
+
+        public String getExtension() {
+            return extension;
+        }
+
+        public String getLanguage() {
+            return "none";
+        }
+
+        public String getPackaging() {
+            return packaging;
+        }
+
+        public boolean isAddedToClasspath() {
+            return false;
+        }
+
+        public boolean isIncludesDependencies() {
+            return false;
+        }
+    }
+}
diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/LifecycleModuleBuilder.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/LifecycleModuleBuilder.java
index a210b07d9..2ecb1d4fd 100644
--- a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/LifecycleModuleBuilder.java
+++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/LifecycleModuleBuilder.java
@@ -30,6 +30,7 @@ import org.apache.maven.execution.ExecutionEvent;
 import org.apache.maven.execution.MavenSession;
 import org.apache.maven.execution.ProjectExecutionEvent;
 import org.apache.maven.execution.ProjectExecutionListener;
+import org.apache.maven.internal.transformation.ConsumerPomArtifactTransformer;
 import org.apache.maven.lifecycle.MavenExecutionPlan;
 import org.apache.maven.lifecycle.internal.builder.BuilderCommon;
 import org.apache.maven.plugin.MojoExecution;
@@ -55,6 +56,7 @@ public class LifecycleModuleBuilder {
     private final BuilderCommon builderCommon;
     private final ExecutionEventCatapult eventCatapult;
     private final ProjectExecutionListener projectExecutionListener;
+    private final ConsumerPomArtifactTransformer consumerPomArtifactTransformer;
     private final SessionScope sessionScope;
 
     @Inject
@@ -63,11 +65,13 @@ public class LifecycleModuleBuilder {
             BuilderCommon builderCommon,
             ExecutionEventCatapult eventCatapult,
             List<ProjectExecutionListener> listeners,
+            ConsumerPomArtifactTransformer consumerPomArtifactTransformer,
             SessionScope sessionScope) {
         this.mojoExecutor = mojoExecutor;
         this.builderCommon = builderCommon;
         this.eventCatapult = eventCatapult;
         this.projectExecutionListener = new CompoundProjectExecutionListener(listeners);
+        this.consumerPomArtifactTransformer = consumerPomArtifactTransformer;
         this.sessionScope = sessionScope;
     }
 
@@ -93,6 +97,8 @@ public class LifecycleModuleBuilder {
                 return;
             }
 
+            consumerPomArtifactTransformer.injectTransformedArtifacts(currentProject, session.getRepositorySession());
+
             BuilderCommon.attachToThread(currentProject);
 
             projectExecutionListener.beforeProjectExecution(new ProjectExecutionEvent(session, currentProject));
diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/builder/BuilderCommon.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/builder/BuilderCommon.java
index 80779df1d..26874e3ac 100644
--- a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/builder/BuilderCommon.java
+++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/builder/BuilderCommon.java
@@ -23,8 +23,6 @@ import javax.inject.Named;
 import javax.inject.Singleton;
 
 import java.util.List;
-import java.util.Optional;
-import java.util.Properties;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -33,7 +31,6 @@ import org.apache.maven.execution.BuildFailure;
 import org.apache.maven.execution.ExecutionEvent;
 import org.apache.maven.execution.MavenExecutionRequest;
 import org.apache.maven.execution.MavenSession;
-import org.apache.maven.feature.Features;
 import org.apache.maven.internal.MultilineMessageHelper;
 import org.apache.maven.lifecycle.LifecycleExecutionException;
 import org.apache.maven.lifecycle.LifecycleNotFoundException;
@@ -112,24 +109,6 @@ public class BuilderCommon {
 
         lifecycleDebugLogger.debugProjectPlan(project, executionPlan);
 
-        // With Maven 4's build/consumer the POM will always rewrite during distribution.
-        // The maven-gpg-plugin uses the original POM, causing an invalid signature.
-        // Fail as long as there's no solution available yet
-        Properties userProperties = session.getUserProperties();
-        if (Features.buildConsumer(userProperties).isActive()) {
-            Optional<MojoExecution> gpgMojo = executionPlan.getMojoExecutions().stream()
-                    .filter(m -> "maven-gpg-plugin".equals(m.getArtifactId())
-                            && "org.apache.maven.plugins".equals(m.getGroupId()))
-                    .findAny();
-
-            if (gpgMojo.isPresent()) {
-                throw new LifecycleExecutionException("The maven-gpg-plugin is not supported by Maven 4."
-                        + " Verify if there is a compatible signing solution,"
-                        + " add -D" + Features.buildConsumer(userProperties).propertyName() + "=false"
-                        + " or use Maven 3.");
-            }
-        }
-
         if (session.getRequest().getDegreeOfConcurrency() > 1
                 && session.getProjects().size() > 1) {
             final Set<Plugin> unsafePlugins = executionPlan.getNonThreadSafePlugins();
diff --git a/maven-core/src/test/java/org/apache/maven/internal/aether/ConsumerModelSourceTransformerTest.java b/maven-core/src/test/java/org/apache/maven/internal/transformation/ConsumerPomArtifactTransformerTest.java
similarity index 87%
rename from maven-core/src/test/java/org/apache/maven/internal/aether/ConsumerModelSourceTransformerTest.java
rename to maven-core/src/test/java/org/apache/maven/internal/transformation/ConsumerPomArtifactTransformerTest.java
index 457cbed26..64f95422b 100644
--- a/maven-core/src/test/java/org/apache/maven/internal/aether/ConsumerModelSourceTransformerTest.java
+++ b/maven-core/src/test/java/org/apache/maven/internal/transformation/ConsumerPomArtifactTransformerTest.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.internal.aether;
+package org.apache.maven.internal.transformation;
 
 import java.io.InputStream;
 import java.nio.file.Files;
@@ -28,9 +28,7 @@ import org.apache.maven.model.building.TransformerContext;
 import org.junit.jupiter.api.Test;
 import org.xmlunit.assertj.XmlAssert;
 
-public class ConsumerModelSourceTransformerTest {
-    private ConsumerModelSourceTransformer transformer = new ConsumerModelSourceTransformer();
-
+public class ConsumerPomArtifactTransformerTest {
     @Test
     public void transform() throws Exception {
         Path beforePomFile =
@@ -39,7 +37,8 @@ public class ConsumerModelSourceTransformerTest {
                 Paths.get("src/test/resources/projects/transform/after.pom").toAbsolutePath();
 
         try (InputStream expected = Files.newInputStream(afterPomFile);
-                InputStream result = transformer.transform(beforePomFile, new NoTransformerContext())) {
+                InputStream result =
+                        ConsumerPomArtifactTransformer.transform(beforePomFile, new NoTransformerContext())) {
             XmlAssert.assertThat(result).and(expected).areIdentical();
         }
     }
diff --git a/maven-core/src/test/java/org/apache/maven/lifecycle/internal/LifecycleModuleBuilderTest.java b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/LifecycleModuleBuilderTest.java
index abda9fddd..c162463f6 100644
--- a/maven-core/src/test/java/org/apache/maven/lifecycle/internal/LifecycleModuleBuilderTest.java
+++ b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/LifecycleModuleBuilderTest.java
@@ -37,6 +37,7 @@ import org.apache.maven.plugin.MojoExecution;
 import org.apache.maven.project.MavenProject;
 import org.codehaus.plexus.PlexusContainer;
 import org.codehaus.plexus.testing.PlexusTest;
+import org.eclipse.aether.DefaultRepositorySystemSession;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -63,7 +64,8 @@ public class LifecycleModuleBuilderTest {
         MavenExecutionRequest mavenExecutionRequest = new DefaultMavenExecutionRequest();
         mavenExecutionRequest.setExecutionListener(new AbstractExecutionListener());
         mavenExecutionRequest.setGoals(Arrays.asList("clean"));
-        final MavenSession session = new MavenSession(null, null, mavenExecutionRequest, defaultMavenExecutionResult);
+        final MavenSession session = new MavenSession(
+                null, new DefaultRepositorySystemSession(), mavenExecutionRequest, defaultMavenExecutionResult);
         final ProjectDependencyGraphStub dependencyGraphStub = new ProjectDependencyGraphStub();
         session.setProjectDependencyGraph(dependencyGraphStub);
         session.setProjects(dependencyGraphStub.getSortedProjects());
diff --git a/maven-model-builder/src/main/java/org/apache/maven/feature/Features.java b/maven-model-builder/src/main/java/org/apache/maven/feature/Features.java
index 2da5b61f4..fba0531e3 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/feature/Features.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/feature/Features.java
@@ -18,7 +18,10 @@
  */
 package org.apache.maven.feature;
 
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Properties;
+import java.util.stream.Collectors;
 
 /**
  * Centralized class for feature information
@@ -30,9 +33,22 @@ public final class Features {
     private Features() {}
 
     public static Feature buildConsumer(Properties userProperties) {
+        return buildConsumer(toMap(userProperties));
+    }
+
+    public static Feature buildConsumer(Map<String, String> userProperties) {
         return new Feature(userProperties, "maven.experimental.buildconsumer", "true");
     }
 
+    private static Map<String, String> toMap(Properties properties) {
+        return properties.entrySet().stream()
+                .collect(Collectors.toMap(
+                        e -> String.valueOf(e.getKey()),
+                        e -> String.valueOf(e.getValue()),
+                        (prev, next) -> next,
+                        HashMap::new));
+    }
+
     /**
      * Represents some feature
      *
@@ -44,9 +60,9 @@ public final class Features {
 
         private final String name;
 
-        Feature(Properties userProperties, String name, String defaultValue) {
+        Feature(Map<String, String> userProperties, String name, String defaultValue) {
             this.name = name;
-            this.active = "true".equals(userProperties.getProperty(name, defaultValue));
+            this.active = "true".equals(userProperties.getOrDefault(name, defaultValue));
         }
 
         public boolean isActive() {