View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.gpg;
20  
21  import javax.inject.Inject;
22  
23  import java.io.IOException;
24  import java.nio.file.Files;
25  import java.nio.file.InvalidPathException;
26  import java.nio.file.Path;
27  import java.nio.file.Paths;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.Collection;
31  import java.util.Collections;
32  import java.util.HashSet;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Set;
36  import java.util.stream.Collectors;
37  import java.util.stream.Stream;
38  
39  import org.apache.maven.plugin.MojoExecutionException;
40  import org.apache.maven.plugin.MojoFailureException;
41  import org.apache.maven.plugins.annotations.Mojo;
42  import org.apache.maven.plugins.annotations.Parameter;
43  import org.codehaus.plexus.util.FileUtils;
44  import org.eclipse.aether.DefaultRepositorySystemSession;
45  import org.eclipse.aether.RepositorySystem;
46  import org.eclipse.aether.RepositorySystemSession;
47  import org.eclipse.aether.RequestTrace;
48  import org.eclipse.aether.artifact.Artifact;
49  import org.eclipse.aether.artifact.DefaultArtifact;
50  import org.eclipse.aether.deployment.DeployRequest;
51  import org.eclipse.aether.deployment.DeploymentException;
52  import org.eclipse.aether.repository.LocalRepository;
53  import org.eclipse.aether.repository.RemoteRepository;
54  import org.eclipse.aether.resolution.ArtifactRequest;
55  import org.eclipse.aether.resolution.ArtifactResolutionException;
56  import org.eclipse.aether.resolution.ArtifactResult;
57  import org.eclipse.aether.util.artifact.SubArtifact;
58  
59  /**
60   * Resolves given artifacts from a given remote repository, signs them, and deploys the signatures next to signed
61   * artifacts, and cleans up afterward. This mojo will use "own" local repository for all the operations to not
62   * "pollute" user local repository, and also to be able to fully clean up (delete) after job done.
63   *
64   * @since 3.2.3
65   */
66  @Mojo(name = "sign-deployed", requiresProject = false, threadSafe = true)
67  public class SignDeployedMojo extends AbstractGpgMojo {
68  
69      /**
70       * URL where the artifacts are deployed.
71       */
72      @Parameter(property = "url", required = true)
73      private String url;
74  
75      /**
76       * Server ID to map on the &lt;id&gt; under &lt;server&gt; section of <code>settings.xml</code>. In most cases, this
77       * parameter will be required for authentication.
78       */
79      @Parameter(property = "repositoryId", required = true)
80      private String repositoryId;
81  
82      /**
83       * Should generate coordinates "javadoc" sub-artifacts?
84       */
85      @Parameter(property = "javadoc", defaultValue = "true", required = true)
86      private boolean javadoc;
87  
88      /**
89       * Should generate coordinates "sources" sub-artifacts?
90       */
91      @Parameter(property = "sources", defaultValue = "true", required = true)
92      private boolean sources;
93  
94      /**
95       * If no {@link ArtifactCollectorSPI} is added, this Mojo will fall back to this parameter to collect GAVs that are
96       * deployed and needs signatures deployed next to them. This parameter can contain multiple things:
97       * <ul>
98       *     <li>A path to an existing file, that contains one GAV spec at a line. File may also contain empty lines or
99       *     lines starting with {@code #} that will be ignored.</li>
100      *     <li>A comma separated list of GAV specs.</li>
101      * </ul>
102      * <p>
103      * Note: format of GAV entries must be {@code <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>}.
104      */
105     @Parameter(property = "artifacts")
106     private String artifacts;
107 
108     private final RepositorySystem repositorySystem;
109 
110     private final Map<String, ArtifactCollectorSPI> artifactCollectors;
111 
112     @Inject
113     public SignDeployedMojo(RepositorySystem repositorySystem, Map<String, ArtifactCollectorSPI> artifactCollectors) {
114         this.repositorySystem = repositorySystem;
115         this.artifactCollectors = artifactCollectors;
116     }
117 
118     @Override
119     protected void doExecute() throws MojoExecutionException, MojoFailureException {
120         if (settings.isOffline()) {
121             throw new MojoFailureException("Cannot deploy artifacts when Maven is in offline mode");
122         }
123 
124         Path tempDirectory = null;
125         Set<Artifact> artifacts = new HashSet<>();
126         try {
127             tempDirectory = Files.createTempDirectory("gpg-sign-deployed");
128             getLog().debug("Using temp directory " + tempDirectory);
129 
130             DefaultRepositorySystemSession signingSession =
131                     new DefaultRepositorySystemSession(session.getRepositorySession());
132             signingSession.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager(
133                     signingSession, new LocalRepository(tempDirectory.toFile())));
134 
135             // remote repo where deployed artifacts are, and where signatures need to be deployed
136             RemoteRepository deploymentRepository = repositorySystem.newDeploymentRepository(
137                     signingSession, new RemoteRepository.Builder(repositoryId, "default", url).build());
138 
139             // get artifacts list
140             getLog().debug("Collecting artifacts for signing...");
141             artifacts.addAll(collectArtifacts(signingSession, deploymentRepository));
142             getLog().info("Collected " + artifacts.size() + " artifact" + ((artifacts.size() > 1) ? "s" : "")
143                     + " for signing");
144 
145             // create additional ones if needed
146             if (sources || javadoc) {
147                 getLog().debug("Adding additional artifacts...");
148                 List<Artifact> additions = new ArrayList<>();
149                 for (Artifact artifact : artifacts) {
150                     if (artifact.getClassifier().isEmpty()) {
151                         if (sources) {
152                             additions.add(new SubArtifact(artifact, "sources", "jar"));
153                         }
154                         if (javadoc) {
155                             additions.add(new SubArtifact(artifact, "javadoc", "jar"));
156                         }
157                     }
158                 }
159                 artifacts.addAll(additions);
160             }
161 
162             // resolve them all
163             getLog().info("Resolving " + artifacts.size() + " artifact" + ((artifacts.size() > 1) ? "s" : "")
164                     + " artifacts for signing...");
165             List<ArtifactResult> results = repositorySystem.resolveArtifacts(
166                     signingSession,
167                     artifacts.stream()
168                             .map(a -> new ArtifactRequest(a, Collections.singletonList(deploymentRepository), "gpg"))
169                             .collect(Collectors.toList()));
170             artifacts = results.stream().map(ArtifactResult::getArtifact).collect(Collectors.toSet());
171 
172             // sign all
173             AbstractGpgSigner signer = newSigner(null);
174             signer.setOutputDirectory(tempDirectory.toFile());
175             getLog().info("Signer '" + signer.signerName() + "' is signing " + artifacts.size() + " file"
176                     + ((artifacts.size() > 1) ? "s" : "") + " with key " + signer.getKeyInfo());
177 
178             HashSet<Artifact> signatures = new HashSet<>();
179             for (Artifact a : artifacts) {
180                 signatures.add(new DefaultArtifact(
181                                 a.getGroupId(),
182                                 a.getArtifactId(),
183                                 a.getClassifier(),
184                                 a.getExtension() + AbstractGpgSigner.SIGNATURE_EXTENSION,
185                                 a.getVersion())
186                         .setFile(signer.generateSignatureForArtifact(a.getFile())));
187             }
188 
189             // deploy all signature
190             getLog().info("Deploying artifact signatures...");
191             repositorySystem.deploy(
192                     signingSession,
193                     new DeployRequest()
194                             .setRepository(deploymentRepository)
195                             .setArtifacts(signatures)
196                             .setTrace(RequestTrace.newChild(null, this)));
197         } catch (IOException e) {
198             throw new MojoExecutionException("IO error: " + e.getMessage(), e);
199         } catch (ArtifactResolutionException e) {
200             throw new MojoExecutionException(
201                     "Error resolving deployed artifacts " + artifacts + ": " + e.getMessage(), e);
202         } catch (DeploymentException e) {
203             throw new MojoExecutionException("Error deploying signatures: " + e.getMessage(), e);
204         } finally {
205             if (tempDirectory != null) {
206                 getLog().info("Cleaning up...");
207                 try {
208                     FileUtils.deleteDirectory(tempDirectory.toFile());
209                 } catch (IOException e) {
210                     getLog().warn("Could not clean up temp directory " + tempDirectory);
211                 }
212             }
213         }
214     }
215 
216     /**
217      * Returns a collection of remotely deployed artifacts that needs to be signed and have signatures deployed
218      * next to them.
219      */
220     protected Collection<Artifact> collectArtifacts(RepositorySystemSession session, RemoteRepository remoteRepository)
221             throws IOException {
222         Collection<Artifact> result = null;
223         for (ArtifactCollectorSPI artifactCollector : artifactCollectors.values()) {
224             result = artifactCollector.collectArtifacts(session, remoteRepository);
225             if (result != null) {
226                 break;
227             }
228         }
229         if (result == null) {
230             if (artifacts != null) {
231                 try {
232                     Path path = Paths.get(artifacts);
233                     if (Files.isRegularFile(path)) {
234                         try (Stream<String> lines = Files.lines(path)) {
235                             result = lines.filter(l -> !l.isEmpty() && !l.startsWith("#"))
236                                     .map(DefaultArtifact::new)
237                                     .collect(Collectors.toSet());
238                         }
239                     }
240                 } catch (InvalidPathException e) {
241                     // ignore
242                 }
243                 if (result == null) {
244                     result = Arrays.stream(artifacts.split(","))
245                             .map(DefaultArtifact::new)
246                             .collect(Collectors.toSet());
247                 }
248             }
249         }
250         if (result == null) {
251             throw new IllegalStateException("No source to collect from (set -Dartifacts=g:a:v... or add collector)");
252         }
253         return result;
254     }
255 }