001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.maven.plugin.plugin; 020 021import javax.inject.Inject; 022 023import java.io.File; 024import java.net.URI; 025import java.util.Arrays; 026import java.util.Collections; 027import java.util.LinkedHashSet; 028import java.util.List; 029import java.util.Set; 030 031import org.apache.maven.artifact.Artifact; 032import org.apache.maven.artifact.resolver.filter.ArtifactFilter; 033import org.apache.maven.artifact.resolver.filter.IncludesArtifactFilter; 034import org.apache.maven.execution.MavenSession; 035import org.apache.maven.plugin.MojoExecutionException; 036import org.apache.maven.plugin.descriptor.InvalidPluginDescriptorException; 037import org.apache.maven.plugin.descriptor.PluginDescriptor; 038import org.apache.maven.plugins.annotations.LifecyclePhase; 039import org.apache.maven.plugins.annotations.Mojo; 040import org.apache.maven.plugins.annotations.Parameter; 041import org.apache.maven.plugins.annotations.ResolutionScope; 042import org.apache.maven.project.MavenProject; 043import org.apache.maven.tools.plugin.DefaultPluginToolsRequest; 044import org.apache.maven.tools.plugin.PluginDescriptorHelper; 045import org.apache.maven.tools.plugin.PluginToolsRequest; 046import org.apache.maven.tools.plugin.extractor.ExtractionException; 047import org.apache.maven.tools.plugin.generator.GeneratorException; 048import org.apache.maven.tools.plugin.generator.GeneratorUtils; 049import org.apache.maven.tools.plugin.generator.PluginDescriptorFilesGenerator; 050import org.apache.maven.tools.plugin.scanner.MojoScanner; 051import org.codehaus.plexus.component.repository.ComponentDependency; 052import org.codehaus.plexus.util.ReaderFactory; 053import org.sonatype.plexus.build.incremental.BuildContext; 054 055/** 056 * <p> 057 * Generate a plugin descriptor. 058 * </p> 059 * <p> 060 * <b>Note:</b> Since 3.0, for Java plugin annotations support, 061 * default <a href="http://maven.apache.org/ref/current/maven-core/lifecycles.html">phase</a> 062 * defined by this goal is after the "compilation" of any scripts. This doesn't override 063 * <a href="/ref/current/maven-core/default-bindings.html#Bindings_for_maven-plugin_packaging">the default binding coded 064 * at generate-resources phase</a> in Maven core. 065 * </p> 066 * @author <a href="mailto:jason@maven.org">Jason van Zyl</a> 067 * @since 2.0 068 */ 069@Mojo( 070 name = "descriptor", 071 defaultPhase = LifecyclePhase.PROCESS_CLASSES, 072 requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, 073 threadSafe = true) 074public class DescriptorGeneratorMojo extends AbstractGeneratorMojo { 075 private static final String VALUE_AUTO = "auto"; 076 077 /** 078 * The directory where the generated <code>plugin.xml</code> file will be put. 079 */ 080 @Parameter(defaultValue = "${project.build.outputDirectory}/META-INF/maven", readonly = true) 081 private File outputDirectory; 082 083 /** 084 * The file encoding of the source files. 085 * 086 * @since 2.5 087 */ 088 @Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}") 089 private String encoding; 090 091 /** 092 * A flag to disable generation of the <code>plugin.xml</code> in favor of a hand authored plugin descriptor. 093 * 094 * @since 2.6 095 */ 096 @Parameter(defaultValue = "false") 097 private boolean skipDescriptor; 098 099 /** 100 * <p> 101 * The role names of mojo extractors to use. 102 * </p> 103 * <p> 104 * If not set, all mojo extractors will be used. If set to an empty extractor name, no mojo extractors 105 * will be used. 106 * </p> 107 * Example: 108 * <pre> 109 * <!-- Use all mojo extractors --> 110 * <extractors/> 111 * 112 * <!-- Use no mojo extractors --> 113 * <extractors> 114 * <extractor/> 115 * </extractors> 116 * 117 * <!-- Use only bsh mojo extractor --> 118 * <extractors> 119 * <extractor>bsh</extractor> 120 * </extractors> 121 * </pre> 122 * The extractors with the following names ship with {@code maven-plugin-tools}: 123 * <ol> 124 * <li>{@code java-annotations}</li> 125 * <li>{@code java-javadoc}, deprecated</li> 126 * <li>{@code ant}, deprecated</li> 127 * <li>{@code bsh}, deprecated</li> 128 * </ol> 129 */ 130 @Parameter 131 private Set<String> extractors; 132 133 /** 134 * By default, an exception is throw if no mojo descriptor is found. As the maven-plugin is defined in core, the 135 * descriptor generator mojo is bound to generate-resources phase. 136 * But for annotations, the compiled classes are needed, so skip error 137 * 138 * @since 3.0 139 */ 140 @Parameter(property = "maven.plugin.skipErrorNoDescriptorsFound", defaultValue = "false") 141 private boolean skipErrorNoDescriptorsFound; 142 143 /** 144 * Flag controlling is "expected dependencies in provided scope" check to be performed or not. Default value: 145 * {@code true}. 146 * 147 * @since 3.6.3 148 */ 149 @Parameter(defaultValue = "true", property = "maven.plugin.checkExpectedProvidedScope") 150 private boolean checkExpectedProvidedScope = true; 151 152 /** 153 * List of {@code groupId} strings of artifact coordinates that are expected to be in "provided" scope. Default 154 * value: {@code ["org.apache.maven"]}. 155 * 156 * @since 3.6.3 157 */ 158 @Parameter 159 private List<String> expectedProvidedScopeGroupIds = Collections.singletonList("org.apache.maven"); 160 161 /** 162 * List of {@code groupId:artifactId} strings of artifact coordinates that are to be excluded from "expected 163 * provided scope" check. Default value: 164 * {@code ["org.apache.maven:maven-archiver", "org.apache.maven:maven-jxr", "org.apache.maven:plexus-utils"]}. 165 * 166 * @since 3.6.3 167 */ 168 @Parameter 169 private List<String> expectedProvidedScopeExclusions = Arrays.asList( 170 "org.apache.maven:maven-archiver", "org.apache.maven:maven-jxr", "org.apache.maven:plexus-utils"); 171 172 /** 173 * Specify the dependencies as {@code groupId:artifactId} containing (abstract) Mojos, to filter 174 * dependencies scanned at runtime and focus on dependencies that are really useful to Mojo analysis. 175 * By default, the value is {@code null} and all dependencies are scanned (as before this parameter was added). 176 * If specified in the configuration with no children, no dependencies are scanned. 177 * 178 * @since 3.5 179 */ 180 @Parameter 181 private List<String> mojoDependencies = null; 182 183 /** 184 * Creates links to existing external javadoc-generated documentation. 185 * <br> 186 * <b>Notes</b>: 187 * all given links should have a fetchable {@code /package-list} or {@code /element-list} file. 188 * For instance: 189 * <pre> 190 * <externalJavadocBaseUrls> 191 * <externalJavadocBaseUrl>https://docs.oracle.com/javase/8/docs/api/</externalJavadocBaseUrl> 192 * </externalJavadocBaseUrls> 193 * </pre> 194 * is valid because <code>https://docs.oracle.com/javase/8/docs/api/package-list</code> exists. 195 * See <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javadoc.html#standard-doclet-options"> 196 * link option of the javadoc tool</a>. 197 * Using this parameter requires connectivity to the given URLs during the goal execution. 198 * @since 3.7.0 199 */ 200 @Parameter(property = "externalJavadocBaseUrls", alias = "links") 201 protected List<URI> externalJavadocBaseUrls; 202 203 /** 204 * The base URL for the Javadoc site containing the current project's API documentation. 205 * This may be relative to the root of the generated Maven site. 206 * It does not need to exist yet at the time when this goal is executed. 207 * Must end with a slash. 208 * <b>In case this is set the javadoc reporting goal should be executed prior to 209 * <a href="../maven-plugin-report-plugin/index.html">Plugin Report</a>.</b> 210 * @since 3.7.0 211 */ 212 @Parameter(property = "internalJavadocBaseUrl") 213 protected URI internalJavadocBaseUrl; 214 215 /** 216 * The version of the javadoc tool (equal to the container JDK version) used to generate the internal javadoc 217 * Only relevant if {@link #internalJavadocBaseUrl} is set. 218 * The default value needs to be overwritten in case toolchains are being used for generating Javadoc. 219 * 220 * @since 3.7.0 221 */ 222 @Parameter(property = "internalJavadocVersion", defaultValue = "${java.version}") 223 protected String internalJavadocVersion; 224 225 /** 226 * The required Java version to set in the plugin descriptor. This is evaluated by Maven 4 and ignored by earlier 227 * Maven versions. Can be either one of the following formats: 228 * 229 * <ul> 230 * <li>A version range which specifies the supported Java versions. It can either use the usual mathematical 231 * syntax like {@code "[1.7,9),[11,)"} or use a single version like {@code "1.8"}. The latter is a short 232 * form for {@code "[1.8,)"}, i.e. denotes the minimum version required.</li> 233 * <li>{@code "auto"} to determine the minimum Java version from the binary class version being generated during 234 * compilation (determined by the extractor).</li> 235 * </ul> 236 * 237 * @since 3.8.0 238 */ 239 @Parameter(defaultValue = VALUE_AUTO) 240 String requiredJavaVersion; 241 242 /** 243 * The required Maven version to set in the plugin descriptor. This is evaluated by Maven 4 and ignored by earlier 244 * Maven versions. Can be either one of the following formats: 245 * 246 * <ul> 247 * <li>A version range which specifies the supported Maven versions. It can either use the usual mathematical 248 * syntax like {@code "[2.0.10,2.1.0),[3.0,)"} or use a single version like {@code "2.2.1"}. The latter is a short 249 * form for {@code "[2.2.1,)"}, i.e. denotes the minimum version required.</li> 250 * <li>{@code "auto"} to determine the minimum Maven version from the POM's Maven prerequisite, or if not set the 251 * referenced Maven Plugin API version.</li> 252 * </ul> 253 * This value takes precedence over the 254 * <a href="https://maven.apache.org/pom.html#Prerequisites">POM's Maven prerequisite</a> in Maven 4. 255 * 256 * @since 3.8.0 257 */ 258 @Parameter(defaultValue = VALUE_AUTO) 259 String requiredMavenVersion; 260 261 private final MavenSession mavenSession; 262 263 /** 264 * The component used for scanning the source tree for mojos. 265 */ 266 private final MojoScanner mojoScanner; 267 268 protected final BuildContext buildContext; 269 270 @Inject 271 public DescriptorGeneratorMojo( 272 MavenProject project, MavenSession mavenSession, MojoScanner mojoScanner, BuildContext buildContext) { 273 super(project); 274 this.mavenSession = mavenSession; 275 this.mojoScanner = mojoScanner; 276 this.buildContext = buildContext; 277 } 278 279 public void generate() throws MojoExecutionException { 280 281 if (!"maven-plugin".equalsIgnoreCase(project.getArtifactId()) 282 && project.getArtifactId().toLowerCase().startsWith("maven-") 283 && project.getArtifactId().toLowerCase().endsWith("-plugin") 284 && !"org.apache.maven.plugins".equals(project.getGroupId())) { 285 getLog().warn(LS + LS + "Artifact Ids of the format maven-___-plugin are reserved for" + LS 286 + "plugins in the Group Id org.apache.maven.plugins" + LS 287 + "Please change your artifactId to the format ___-maven-plugin" + LS 288 + "In the future this error will break the build." + LS + LS); 289 } 290 291 if (skipDescriptor) { 292 getLog().warn("Execution skipped"); 293 return; 294 } 295 296 if (checkExpectedProvidedScope) { 297 Set<Artifact> wrongScopedArtifacts = dependenciesNotInProvidedScope(); 298 if (!wrongScopedArtifacts.isEmpty()) { 299 StringBuilder message = new StringBuilder( 300 LS + LS + "Some dependencies of Maven Plugins are expected to be in provided scope." + LS 301 + "Please make sure that dependencies listed below declared in POM" + LS 302 + "have set '<scope>provided</scope>' as well." + LS + LS 303 + "The following dependencies are in wrong scope:" + LS); 304 for (Artifact artifact : wrongScopedArtifacts) { 305 message.append(" * ").append(artifact).append(LS); 306 } 307 message.append(LS).append(LS); 308 309 getLog().warn(message.toString()); 310 } 311 } 312 313 mojoScanner.setActiveExtractors(extractors); 314 315 // TODO: could use this more, eg in the writing of the plugin descriptor! 316 PluginDescriptor pluginDescriptor = new PluginDescriptor(); 317 318 pluginDescriptor.setGroupId(project.getGroupId()); 319 320 pluginDescriptor.setArtifactId(project.getArtifactId()); 321 322 pluginDescriptor.setVersion(project.getVersion()); 323 324 pluginDescriptor.setGoalPrefix(goalPrefix); 325 326 pluginDescriptor.setName(project.getName()); 327 328 pluginDescriptor.setDescription(project.getDescription()); 329 330 if (encoding == null || encoding.length() < 1) { 331 getLog().warn("Using platform encoding (" + ReaderFactory.FILE_ENCODING 332 + " actually) to read mojo source files, i.e. build is platform dependent!"); 333 } else { 334 getLog().info("Using '" + encoding + "' encoding to read mojo source files."); 335 } 336 337 if (internalJavadocBaseUrl != null && !internalJavadocBaseUrl.getPath().endsWith("/")) { 338 throw new MojoExecutionException("Given parameter 'internalJavadocBaseUrl' must end with a slash but is '" 339 + internalJavadocBaseUrl + "'"); 340 } 341 try { 342 List<ComponentDependency> deps = GeneratorUtils.toComponentDependencies(project.getArtifacts()); 343 pluginDescriptor.setDependencies(deps); 344 345 PluginToolsRequest request = new DefaultPluginToolsRequest(project, pluginDescriptor); 346 request.setEncoding(encoding); 347 request.setSkipErrorNoDescriptorsFound(skipErrorNoDescriptorsFound); 348 request.setDependencies(filterMojoDependencies()); 349 request.setRepoSession(mavenSession.getRepositorySession()); 350 request.setInternalJavadocBaseUrl(internalJavadocBaseUrl); 351 request.setInternalJavadocVersion(internalJavadocVersion); 352 request.setExternalJavadocBaseUrls(externalJavadocBaseUrls); 353 request.setSettings(mavenSession.getSettings()); 354 355 mojoScanner.populatePluginDescriptor(request); 356 request.setPluginDescriptor(extendPluginDescriptor(request)); 357 358 outputDirectory.mkdirs(); 359 360 PluginDescriptorFilesGenerator pluginDescriptorGenerator = new PluginDescriptorFilesGenerator(); 361 pluginDescriptorGenerator.execute(outputDirectory, request); 362 363 buildContext.refresh(outputDirectory); 364 } catch (GeneratorException e) { 365 throw new MojoExecutionException("Error writing plugin descriptor", e); 366 } catch (InvalidPluginDescriptorException | ExtractionException e) { 367 throw new MojoExecutionException( 368 "Error extracting plugin descriptor: '" + e.getLocalizedMessage() + "'", e); 369 } catch (LinkageError e) { 370 throw new MojoExecutionException( 371 "The API of the mojo scanner is not compatible with this plugin version." 372 + " Please check the plugin dependencies configured" 373 + " in the POM and ensure the versions match.", 374 e); 375 } 376 } 377 378 private PluginDescriptor extendPluginDescriptor(PluginToolsRequest request) { 379 PluginDescriptor pluginDescriptor = request.getPluginDescriptor(); 380 pluginDescriptor.setRequiredMavenVersion(getRequiredMavenVersion(request)); 381 return PluginDescriptorHelper.setRequiredJavaVersion(pluginDescriptor, getRequiredJavaVersion(request)); 382 } 383 384 private String getRequiredMavenVersion(PluginToolsRequest request) { 385 if (!VALUE_AUTO.equals(requiredMavenVersion)) { 386 return requiredMavenVersion; 387 } 388 getLog().debug("Trying to derive Maven version automatically from project prerequisites..."); 389 String requiredMavenVersion = 390 project.getPrerequisites() != null ? project.getPrerequisites().getMaven() : null; 391 if (requiredMavenVersion == null) { 392 getLog().debug("Trying to derive Maven version automatically from referenced Maven Plugin API artifact " 393 + "version..."); 394 requiredMavenVersion = request.getUsedMavenApiVersion(); 395 } 396 if (requiredMavenVersion == null) { 397 getLog().warn("Cannot determine the required Maven version automatically, it is recommended to " 398 + "configure some explicit value manually."); 399 } 400 return requiredMavenVersion; 401 } 402 403 private String getRequiredJavaVersion(PluginToolsRequest request) { 404 if (!VALUE_AUTO.equals(requiredJavaVersion)) { 405 return requiredJavaVersion; 406 } 407 String minRequiredJavaVersion = request.getRequiredJavaVersion(); 408 if (minRequiredJavaVersion == null) { 409 getLog().warn("Cannot determine the minimally required Java version automatically, it is recommended to " 410 + "configure some explicit value manually."); 411 return null; 412 } 413 414 return minRequiredJavaVersion; 415 } 416 417 /** 418 * Collects all dependencies expected to be in "provided" scope but are NOT in "provided" scope. 419 */ 420 private Set<Artifact> dependenciesNotInProvidedScope() { 421 LinkedHashSet<Artifact> wrongScopedDependencies = new LinkedHashSet<>(); 422 423 for (Artifact dependency : project.getArtifacts()) { 424 String ga = dependency.getGroupId() + ":" + dependency.getArtifactId(); 425 if (expectedProvidedScopeGroupIds.contains(dependency.getGroupId()) 426 && !expectedProvidedScopeExclusions.contains(ga) 427 && !Artifact.SCOPE_PROVIDED.equals(dependency.getScope())) { 428 wrongScopedDependencies.add(dependency); 429 } 430 } 431 432 return wrongScopedDependencies; 433 } 434 435 /** 436 * Get dependencies filtered with mojoDependencies configuration. 437 * 438 * @return eventually filtered dependencies, or even <code>null</code> if configured with empty mojoDependencies 439 * list 440 * @see #mojoDependencies 441 */ 442 private Set<Artifact> filterMojoDependencies() { 443 Set<Artifact> filteredArtifacts; 444 if (mojoDependencies == null) { 445 filteredArtifacts = new LinkedHashSet<>(project.getArtifacts()); 446 } else if (mojoDependencies.isEmpty()) { 447 filteredArtifacts = null; 448 } else { 449 filteredArtifacts = new LinkedHashSet<>(); 450 451 ArtifactFilter filter = new IncludesArtifactFilter(mojoDependencies); 452 453 for (Artifact artifact : project.getArtifacts()) { 454 if (filter.include(artifact)) { 455 filteredArtifacts.add(artifact); 456 } 457 } 458 } 459 460 return filteredArtifacts; 461 } 462}