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