From 50222d351ac12e746ce9921a957654e5e24a55de Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Wed, 3 Apr 2024 16:49:12 +0200 Subject: [PATCH] [MGPG-120] New mojo sign-deployed (#88) New mojo, "sign-deployed" that is able to sign already deployed artifacts. Assuming there is no Maven project, hence mojo should not require project, just a list of artifacts. --- https://issues.apache.org/jira/browse/MGPG-120 --- pom.xml | 7 + src/it/sign-deployed/invoker.properties | 19 ++ .../org/foo/bar/1.0/bar-1.0-javadoc.jar | Bin 0 -> 424 bytes .../org/foo/bar/1.0/bar-1.0-sources.jar | Bin 0 -> 424 bytes .../org/foo/bar/1.0/bar-1.0-src.tar.gz | Bin 0 -> 357 bytes .../remote-repo/org/foo/bar/1.0/bar-1.0.jar | Bin 0 -> 345 bytes .../remote-repo/org/foo/bar/1.0/bar-1.0.pom | 34 +++ .../org/foo/bar/1.0/bar-1.0.tar.gz | Bin 0 -> 357 bytes .../remote-repo/org/foo/bar/1.0/bar-1.0.zip | Bin 0 -> 345 bytes src/it/sign-deployed/test.properties | 20 ++ src/it/sign-deployed/verify.groovy | 39 +++ .../plugins/gpg/ArtifactCollectorSPI.java | 41 +++ .../plugins/gpg/SignAndDeployFileMojo.java | 2 +- .../maven/plugins/gpg/SignDeployedMojo.java | 252 ++++++++++++++++++ 14 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 src/it/sign-deployed/invoker.properties create mode 100644 src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0-javadoc.jar create mode 100644 src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0-sources.jar create mode 100644 src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0-src.tar.gz create mode 100644 src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.jar create mode 100644 src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.pom create mode 100644 src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.tar.gz create mode 100644 src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.zip create mode 100644 src/it/sign-deployed/test.properties create mode 100644 src/it/sign-deployed/verify.groovy create mode 100644 src/main/java/org/apache/maven/plugins/gpg/ArtifactCollectorSPI.java create mode 100644 src/main/java/org/apache/maven/plugins/gpg/SignDeployedMojo.java diff --git a/pom.xml b/pom.xml index a60987f..068e024 100644 --- a/pom.xml +++ b/pom.xml @@ -123,6 +123,13 @@ under the License. ${resolverVersion} provided + + org.apache.maven.resolver + maven-resolver-util + ${resolverVersion} + + compile + org.apache.maven.plugin-tools maven-plugin-annotations diff --git a/src/it/sign-deployed/invoker.properties b/src/it/sign-deployed/invoker.properties new file mode 100644 index 0000000..ef0c219 --- /dev/null +++ b/src/it/sign-deployed/invoker.properties @@ -0,0 +1,19 @@ +# 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. + +invoker.goals = ${project.groupId}:${project.artifactId}:${project.version}:sign-deployed +invoker.environmentVariables.MAVEN_GPG_PASSPHRASE = TEST diff --git a/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0-javadoc.jar b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0-javadoc.jar new file mode 100644 index 0000000000000000000000000000000000000000..d53f442a9bf7b75157288e1ffec00c2fd4901e6e GIT binary patch literal 424 zcmWIWW@Zs#-~dALfbDh+NPv@pg~8V~#8KDN&rSc|DFy~+h5&DN4v-2asImZ@nni#r z;F^6M{XE@VgG2Ou-9G!CIql=Et9OytTUYDcne&^246YbIcv__A<*VcAd$DvC3+IfN zl1HRxYGhc5i9A`NRq;&qb>^p{k421N+iU(X9kc^p{k421N+iU(X9kcW>}kpdJ9L68;%$VQ zVZmiv+kyKe`WgI`rmmY|)ii)&h$rI}33QqoLK?v;+#jWffB!!PD3_OXipM6k?18w9Oh zx6|&2Ea+_ZALcw<$623$TiB)i3oe?3^35*eb32}Ya!@-Y`{o|Vh+g&SEiL4I~vgBPJ)42gRl{nMj?*T9IiZKSqCfMSOol z+l7x-^}n_1|Gxe$?v?7_ahn45{~H3Siw + + + + + 4.0.0 + + org.apache.maven.its.gpg.sadfs + test + 1.0 + jar + + MGPG-12 + + Tests the signing and deployment of a simple release JAR along with its POM. + + diff --git a/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.tar.gz b/src/it/sign-deployed/remote-repo/org/foo/bar/1.0/bar-1.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..ad736b0d7b2e3120ed3a365a1cffe9cfbbd4e85c GIT binary patch literal 357 zcmV-r0h<0FiwFQcO#n^+1MSm6OT#b}2kW>}kpdJ9L68;%$VQ zVZmiv+kyKe`WgI`rmmY|)ii)&h$rI}33QqoLK?v;+#jWffB!!PD3_OXipM6k?18w9Oh zx6|&2Ea+_ZALcw<$623$TiB)i3oe?3^35*eb32}Ya!@-Y`{o|Vh+g&SEiL4I~vgBPJ)42gRl{nMj?*T9IiZKSqCfMSOol z+l7x-^}n_1|Gxe$?v?7_ahn45{~H3Siw + * Collector should collect only relevant artifacts, those that are subject to signing. + */ + Collection collectArtifacts(RepositorySystemSession session, RemoteRepository remoteRepository) + throws IOException; +} diff --git a/src/main/java/org/apache/maven/plugins/gpg/SignAndDeployFileMojo.java b/src/main/java/org/apache/maven/plugins/gpg/SignAndDeployFileMojo.java index 12b191f..e4853fc 100644 --- a/src/main/java/org/apache/maven/plugins/gpg/SignAndDeployFileMojo.java +++ b/src/main/java/org/apache/maven/plugins/gpg/SignAndDeployFileMojo.java @@ -58,7 +58,7 @@ import org.eclipse.aether.repository.RemoteRepository; /** - * Signs artifacts and installs the artifact in the remote repository. + * Signs artifacts and deploys the artifacts and signatures in the remote repository. * * @author Daniel Kulp * @since 1.0-beta-4 diff --git a/src/main/java/org/apache/maven/plugins/gpg/SignDeployedMojo.java b/src/main/java/org/apache/maven/plugins/gpg/SignDeployedMojo.java new file mode 100644 index 0000000..a05c230 --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/gpg/SignDeployedMojo.java @@ -0,0 +1,252 @@ +/* + * 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.plugins.gpg; + +import javax.inject.Inject; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.codehaus.plexus.util.FileUtils; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.RequestTrace; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.deployment.DeployRequest; +import org.eclipse.aether.deployment.DeploymentException; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.ArtifactRequest; +import org.eclipse.aether.resolution.ArtifactResolutionException; +import org.eclipse.aether.resolution.ArtifactResult; +import org.eclipse.aether.util.artifact.SubArtifact; + +/** + * Resolves given artifacts from a given remote repository, signs them, and deploys the signatures next to signed + * artifacts, and cleans up afterward. This mojo will use "own" local repository for all the operations to not + * "pollute" user local repository, and also to be able to fully clean up (delete) after job done. + * + * @since 3.2.3 + */ +@Mojo(name = "sign-deployed", requiresProject = false, threadSafe = true) +public class SignDeployedMojo extends AbstractGpgMojo { + + /** + * URL where the artifacts are deployed. + */ + @Parameter(property = "url", required = true) + private String url; + + /** + * Server ID to map on the <id> under <server> section of settings.xml. In most cases, this + * parameter will be required for authentication. + */ + @Parameter(property = "repositoryId", required = true) + private String repositoryId; + + /** + * Should generate coordinates "javadoc" sub-artifacts? + */ + @Parameter(property = "javadoc", defaultValue = "true", required = true) + private boolean javadoc; + + /** + * Should generate coordinates "sources" sub-artifacts? + */ + @Parameter(property = "sources", defaultValue = "true", required = true) + private boolean sources; + + /** + * If no {@link ArtifactCollectorSPI} is added, this Mojo will fall back to this parameter to collect GAVs that are + * deployed and needs signatures deployed next to them. This parameter can contain multiple things: + *
    + *
  • A path to an existing file, that contains one GAV spec at a line. File may also contain empty lines or + * lines starting with {@code #} that will be ignored.
  • + *
  • A comma separated list of GAV specs.
  • + *
+ *

+ * Note: format of GAV entries must be {@code :[:[:]]:}. + */ + @Parameter(property = "artifacts") + private String artifacts; + + @Component + private RepositorySystem repositorySystem; + + @Inject + private Map artifactCollectors; + + @Override + protected void doExecute() throws MojoExecutionException, MojoFailureException { + if (settings.isOffline()) { + throw new MojoFailureException("Cannot deploy artifacts when Maven is in offline mode"); + } + + Path tempDirectory = null; + Set artifacts = new HashSet<>(); + try { + tempDirectory = Files.createTempDirectory("gpg-sign-deployed"); + getLog().debug("Using temp directory " + tempDirectory); + + DefaultRepositorySystemSession signingSession = + new DefaultRepositorySystemSession(session.getRepositorySession()); + signingSession.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager( + signingSession, new LocalRepository(tempDirectory.toFile()))); + + // remote repo where deployed artifacts are, and where signatures need to be deployed + RemoteRepository deploymentRepository = repositorySystem.newDeploymentRepository( + signingSession, new RemoteRepository.Builder(repositoryId, "default", url).build()); + + // get artifacts list + getLog().debug("Collecting artifacts for signing..."); + artifacts.addAll(collectArtifacts(signingSession, deploymentRepository)); + getLog().info("Collected " + artifacts.size() + " artifact" + ((artifacts.size() > 1) ? "s" : "") + + " for signing"); + + // create additional ones if needed + if (sources || javadoc) { + getLog().debug("Adding additional artifacts..."); + List additions = new ArrayList<>(); + for (Artifact artifact : artifacts) { + if (artifact.getClassifier().isEmpty()) { + if (sources) { + additions.add(new SubArtifact(artifact, "sources", "jar")); + } + if (javadoc) { + additions.add(new SubArtifact(artifact, "javadoc", "jar")); + } + } + } + artifacts.addAll(additions); + } + + // resolve them all + getLog().info("Resolving " + artifacts.size() + " artifact" + ((artifacts.size() > 1) ? "s" : "") + + " artifacts for signing..."); + List results = repositorySystem.resolveArtifacts( + signingSession, + artifacts.stream() + .map(a -> new ArtifactRequest(a, Collections.singletonList(deploymentRepository), "gpg")) + .collect(Collectors.toList())); + artifacts = results.stream().map(ArtifactResult::getArtifact).collect(Collectors.toSet()); + + // sign all + AbstractGpgSigner signer = newSigner(null); + signer.setOutputDirectory(tempDirectory.toFile()); + getLog().info("Signer '" + signer.signerName() + "' is signing " + artifacts.size() + " file" + + ((artifacts.size() > 1) ? "s" : "") + " with key " + signer.getKeyInfo()); + + HashSet signatures = new HashSet<>(); + for (Artifact a : artifacts) { + signatures.add(new DefaultArtifact( + a.getGroupId(), + a.getArtifactId(), + a.getClassifier(), + a.getExtension() + AbstractGpgSigner.SIGNATURE_EXTENSION, + a.getVersion()) + .setFile(signer.generateSignatureForArtifact(a.getFile()))); + } + + // deploy all signature + getLog().info("Deploying artifact signatures..."); + repositorySystem.deploy( + signingSession, + new DeployRequest() + .setRepository(deploymentRepository) + .setArtifacts(signatures) + .setTrace(RequestTrace.newChild(null, this))); + } catch (IOException e) { + throw new MojoExecutionException("IO error: " + e.getMessage(), e); + } catch (ArtifactResolutionException e) { + throw new MojoExecutionException( + "Error resolving deployed artifacts " + artifacts + ": " + e.getMessage(), e); + } catch (DeploymentException e) { + throw new MojoExecutionException("Error deploying signatures: " + e.getMessage(), e); + } finally { + if (tempDirectory != null) { + getLog().info("Cleaning up..."); + try { + FileUtils.deleteDirectory(tempDirectory.toFile()); + } catch (IOException e) { + getLog().warn("Could not clean up temp directory " + tempDirectory); + } + } + } + } + + /** + * Returns a collection of remotely deployed artifacts that needs to be signed and have signatures deployed + * next to them. + */ + protected Collection collectArtifacts(RepositorySystemSession session, RemoteRepository remoteRepository) + throws IOException { + Collection result = null; + for (ArtifactCollectorSPI artifactCollector : artifactCollectors.values()) { + result = artifactCollector.collectArtifacts(session, remoteRepository); + if (result != null) { + break; + } + } + if (result == null) { + if (artifacts != null) { + try { + Path path = Paths.get(artifacts); + if (Files.isRegularFile(path)) { + try (Stream lines = Files.lines(path)) { + result = lines.filter(l -> !l.isEmpty() && !l.startsWith("#")) + .map(DefaultArtifact::new) + .collect(Collectors.toSet()); + } + } + } catch (InvalidPathException e) { + // ignore + } + if (result == null) { + result = Arrays.stream(artifacts.split(",")) + .map(DefaultArtifact::new) + .collect(Collectors.toSet()); + } + } + } + if (result == null) { + throw new IllegalStateException("No source to collect from (set -Dartifacts=g:a:v... or add collector)"); + } + return result; + } +}