001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.release.plugin.mojos; 018 019import java.io.File; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.OutputStream; 023import java.io.PrintWriter; 024import java.nio.file.Files; 025import java.util.ArrayList; 026import java.util.Collections; 027import java.util.HashSet; 028import java.util.List; 029import java.util.Locale; 030import java.util.Set; 031 032import org.apache.commons.codec.digest.DigestUtils; 033import org.apache.commons.collections4.properties.SortedProperties; 034import org.apache.commons.lang3.StringUtils; 035import org.apache.commons.lang3.reflect.MethodUtils; 036import org.apache.commons.release.plugin.SharedFunctions; 037import org.apache.maven.artifact.Artifact; 038import org.apache.maven.plugin.AbstractMojo; 039import org.apache.maven.plugin.MojoExecutionException; 040import org.apache.maven.plugins.annotations.LifecyclePhase; 041import org.apache.maven.plugins.annotations.Mojo; 042import org.apache.maven.plugins.annotations.Parameter; 043import org.apache.maven.project.MavenProject; 044 045/** 046 * The purpose of this Maven mojo is to detach the artifacts generated by the maven-assembly-plugin, 047 * which for the Apache Commons Project do not get uploaded to Nexus, and putting those artifacts 048 * in the dev distribution location for Apache projects. 049 * 050 * @author chtompki 051 * @since 1.0 052 */ 053@Mojo(name = "detach-distributions", 054 defaultPhase = LifecyclePhase.VERIFY, 055 threadSafe = true, 056 aggregator = true) 057public class CommonsDistributionDetachmentMojo extends AbstractMojo { 058 059 /** 060 * A list of "artifact types" in the Maven vernacular, to 061 * be detached from the deployment. For the time being we want 062 * all artifacts generated by the maven-assembly-plugin to be detached 063 * from the deployment, namely *-src.zip, *-src.tar.gz, *-bin.zip, 064 * *-bin.tar.gz, and the corresponding .asc pgp signatures. 065 */ 066 private static final Set<String> ARTIFACT_TYPES_TO_DETACH; 067 static { 068 final Set<String> hashSet = new HashSet<>(); 069 hashSet.add("zip"); 070 hashSet.add("tar.gz"); 071 hashSet.add("zip.asc"); 072 hashSet.add("tar.gz.asc"); 073 ARTIFACT_TYPES_TO_DETACH = Collections.unmodifiableSet(hashSet); 074 } 075 076 /** 077 * This list is supposed to hold the Maven references to the aforementioned artifacts so that we 078 * can upload them to svn after they've been detached from the Maven deployment. 079 */ 080 private final List<Artifact> detachedArtifacts = new ArrayList<>(); 081 082 /** 083 * A {@link SortedProperties} of {@link Artifact} → {@link String} containing the sha512 signatures 084 * for the individual artifacts, where the {@link Artifact} is represented as: 085 * <code>groupId:artifactId:version:type=sha512</code>. 086 */ 087 private final SortedProperties artifactSha512s = new SortedProperties(); 088 089 /** 090 * The maven project context injection so that we can get a hold of the variables at hand. 091 */ 092 @Parameter(defaultValue = "${project}", required = true) 093 private MavenProject project; 094 095 /** 096 * The working directory in <code>target</code> that we use as a sandbox for the plugin. 097 */ 098 @Parameter(defaultValue = "${project.build.directory}/commons-release-plugin", 099 property = "commons.outputDirectory") 100 private File workingDirectory; 101 102 /** 103 * The subversion staging url to which we upload all of our staged artifacts. 104 */ 105 @Parameter(defaultValue = "", property = "commons.distSvnStagingUrl") 106 private String distSvnStagingUrl; 107 108 /** 109 * A parameter to generally avoid running unless it is specifically turned on by the consuming module. 110 */ 111 @Parameter(defaultValue = "false", property = "commons.release.isDistModule") 112 private Boolean isDistModule; 113 114 @Override 115 public void execute() throws MojoExecutionException { 116 if (!isDistModule) { 117 getLog().info("This module is marked as a non distribution or assembly module, and the plugin will not run."); 118 return; 119 } 120 if (StringUtils.isEmpty(distSvnStagingUrl)) { 121 getLog().warn("commons.distSvnStagingUrl is not set, the commons-release-plugin will not run."); 122 return; 123 } 124 getLog().info("Detaching Assemblies"); 125 for (final Object attachedArtifact : project.getAttachedArtifacts()) { 126 putAttachedArtifactInSha512Map((Artifact) attachedArtifact); 127 if (ARTIFACT_TYPES_TO_DETACH.contains(((Artifact) attachedArtifact).getType())) { 128 detachedArtifacts.add((Artifact) attachedArtifact); 129 } 130 } 131 if (detachedArtifacts.isEmpty()) { 132 getLog().info("Current project contains no distributions. Not executing."); 133 return; 134 } 135 // 136 // PROBLEM CODE for Maven >= 3.8.3 137 // https://issues.apache.org/jira/browse/MNG-7316 138 try { 139 // (1) Try the normal way 140 // Maven 3.8.3 throws an exception here because MavenProject.getAttachedArtifacts() 141 // returns an IMMUTABLE collection. 142 project.getAttachedArtifacts().removeAll(detachedArtifacts); 143 } catch (final UnsupportedOperationException e) { 144 // (2) HACK workaround for https://issues.apache.org/jira/browse/MNG-7316 145 final ArrayList<Artifact> arrayList = new ArrayList<>(project.getAttachedArtifacts()); 146 arrayList.removeAll(detachedArtifacts); 147 try { 148 // MavenProject#setAttachedArtifacts(List) is protected 149 MethodUtils.invokeMethod(project, true, "setAttachedArtifacts", arrayList); 150 } catch (final ReflectiveOperationException roe) { 151 throw new MojoExecutionException(roe); 152 } 153 } 154 if (!workingDirectory.exists()) { 155 SharedFunctions.initDirectory(getLog(), workingDirectory); 156 } 157 writeAllArtifactsInSha512PropertiesFile(); 158 copyRemovedArtifactsToWorkingDirectory(); 159 getLog().info(""); 160 hashArtifacts(); 161 } 162 163 /** 164 * Takes an attached artifact and puts the signature in the map. 165 * @param artifact is a Maven {@link Artifact} taken from the project at start time of mojo. 166 * @throws MojoExecutionException if an {@link IOException} occurs when getting the sha512 of the 167 * artifact. 168 */ 169 private void putAttachedArtifactInSha512Map(final Artifact artifact) throws MojoExecutionException { 170 try { 171 final String artifactKey = getArtifactKey(artifact); 172 if (!artifactKey.endsWith(".asc")) { // .asc files don't need hashes 173 try (InputStream fis = Files.newInputStream(artifact.getFile().toPath())) { 174 artifactSha512s.put(artifactKey, DigestUtils.sha512Hex(fis)); 175 } 176 } 177 } catch (final IOException e) { 178 throw new MojoExecutionException( 179 "Could not find artifact signature for: " 180 + artifact.getArtifactId() 181 + "-" 182 + artifact.getClassifier() 183 + "-" 184 + artifact.getVersion() 185 + " type: " 186 + artifact.getType(), 187 e); 188 } 189 } 190 191 /** 192 * Writes to ./target/commons-release-plugin/sha512.properties the artifact sha512's. 193 * 194 * @throws MojoExecutionException if we can't write the file due to an {@link IOException}. 195 */ 196 private void writeAllArtifactsInSha512PropertiesFile() throws MojoExecutionException { 197 final File propertiesFile = new File(workingDirectory, "sha512.properties"); 198 getLog().info("Writing " + propertiesFile); 199 try (OutputStream fileWriter = Files.newOutputStream(propertiesFile.toPath())) { 200 artifactSha512s.store(fileWriter, "Release SHA-512s"); 201 } catch (final IOException e) { 202 throw new MojoExecutionException("Failure to write SHA-512's", e); 203 } 204 } 205 206 /** 207 * A helper method to copy the newly detached artifacts to <code>target/commons-release-plugin</code> 208 * so that the {@link CommonsDistributionStagingMojo} can find the artifacts later. 209 * 210 * @throws MojoExecutionException if some form of an {@link IOException} occurs, we want it 211 * properly wrapped so that Maven can handle it. 212 */ 213 private void copyRemovedArtifactsToWorkingDirectory() throws MojoExecutionException { 214 final String wdAbsolutePath = workingDirectory.getAbsolutePath(); 215 getLog().info( 216 "Copying " + detachedArtifacts.size() + " detached artifacts to working directory " + wdAbsolutePath); 217 for (final Artifact artifact: detachedArtifacts) { 218 final File artifactFile = artifact.getFile(); 219 final StringBuilder copiedArtifactAbsolutePath = new StringBuilder(wdAbsolutePath); 220 copiedArtifactAbsolutePath.append("/"); 221 copiedArtifactAbsolutePath.append(artifactFile.getName()); 222 final File copiedArtifact = new File(copiedArtifactAbsolutePath.toString()); 223 getLog().info("Copying: " + artifactFile.getName()); 224 SharedFunctions.copyFile(getLog(), artifactFile, copiedArtifact); 225 } 226 } 227 228 /** 229 * A helper method that creates sha512 signature files for our detached artifacts in the 230 * <code>target/commons-release-plugin</code> directory for the purpose of being uploaded by 231 * the {@link CommonsDistributionStagingMojo}. 232 * 233 * @throws MojoExecutionException if some form of an {@link IOException} occurs, we want it 234 * properly wrapped so that Maven can handle it. 235 */ 236 private void hashArtifacts() throws MojoExecutionException { 237 for (final Artifact artifact : detachedArtifacts) { 238 if (!artifact.getFile().getName().toLowerCase(Locale.ROOT).contains("asc")) { 239 final String artifactKey = getArtifactKey(artifact); 240 try { 241 final String digest; 242 // SHA-512 243 digest = artifactSha512s.getProperty(artifactKey.toString()); 244 getLog().info(artifact.getFile().getName() + " sha512: " + digest); 245 try (PrintWriter printWriter = new PrintWriter( 246 getSha512FilePath(workingDirectory, artifact.getFile()))) { 247 printWriter.println(digest); 248 } 249 } catch (final IOException e) { 250 throw new MojoExecutionException("Could not sign file: " + artifact.getFile().getName(), e); 251 } 252 } 253 } 254 } 255 256 /** 257 * A helper method to create a file path for the <code>sha512</code> signature file from a given file. 258 * 259 * @param directory is the {@link File} for the directory in which to make the <code>.sha512</code> file. 260 * @param file the {@link File} whose name we should use to create the <code>.sha512</code> file. 261 * @return a {@link String} that is the absolute path to the <code>.sha512</code> file. 262 */ 263 private String getSha512FilePath(final File directory, final File file) { 264 final StringBuilder buffer = new StringBuilder(directory.getAbsolutePath()); 265 buffer.append("/"); 266 buffer.append(file.getName()); 267 buffer.append(".sha512"); 268 return buffer.toString(); 269 } 270 271 /** 272 * Generates the unique artifact key for storage in our sha512 map. For example, 273 * commons-test-1.4-src.tar.gz should have it's name as the key. 274 * 275 * @param artifact the {@link Artifact} that we wish to generate a key for. 276 * @return the generated key 277 */ 278 private String getArtifactKey(final Artifact artifact) { 279 return artifact.getFile().getName(); 280 } 281}