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 java.io.File; 022import java.net.URI; 023import java.util.Arrays; 024import java.util.Collections; 025import java.util.LinkedHashSet; 026import java.util.List; 027import java.util.Set; 028 029import org.apache.maven.artifact.Artifact; 030import org.apache.maven.artifact.resolver.filter.ArtifactFilter; 031import org.apache.maven.artifact.resolver.filter.IncludesArtifactFilter; 032import org.apache.maven.plugin.MojoExecutionException; 033import org.apache.maven.plugin.descriptor.InvalidPluginDescriptorException; 034import org.apache.maven.plugin.descriptor.PluginDescriptor; 035import org.apache.maven.plugins.annotations.Component; 036import org.apache.maven.plugins.annotations.LifecyclePhase; 037import org.apache.maven.plugins.annotations.Mojo; 038import org.apache.maven.plugins.annotations.Parameter; 039import org.apache.maven.plugins.annotations.ResolutionScope; 040import org.apache.maven.settings.Settings; 041import org.apache.maven.tools.plugin.DefaultPluginToolsRequest; 042import org.apache.maven.tools.plugin.ExtendedPluginDescriptor; 043import org.apache.maven.tools.plugin.PluginToolsRequest; 044import org.apache.maven.tools.plugin.extractor.ExtractionException; 045import org.apache.maven.tools.plugin.generator.GeneratorException; 046import org.apache.maven.tools.plugin.generator.GeneratorUtils; 047import org.apache.maven.tools.plugin.generator.PluginDescriptorFilesGenerator; 048import org.apache.maven.tools.plugin.scanner.MojoScanner; 049import org.codehaus.plexus.component.repository.ComponentDependency; 050import org.codehaus.plexus.util.ReaderFactory; 051import org.eclipse.aether.RepositorySystemSession; 052import org.sonatype.plexus.build.incremental.BuildContext; 053 054/** 055 * <p> 056 * Generate a plugin descriptor. 057 * </p> 058 * <p> 059 * <b>Note:</b> Since 3.0, for Java plugin annotations support, 060 * default <a href="http://maven.apache.org/ref/current/maven-core/lifecycles.html">phase</a> 061 * defined by this goal is after the "compilation" of any scripts. This doesn't override 062 * <a href="/ref/current/maven-core/default-bindings.html#Bindings_for_maven-plugin_packaging">the default binding coded 063 * at generate-resources phase</a> in Maven core. 064 * </p> 065 * @author <a href="mailto:jason@maven.org">Jason van Zyl</a> 066 * @since 2.0 067 */ 068@Mojo( 069 name = "descriptor", 070 defaultPhase = LifecyclePhase.PROCESS_CLASSES, 071 requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, 072 threadSafe = true) 073public class DescriptorGeneratorMojo extends AbstractGeneratorMojo { 074 private static final String VALUE_AUTO = "auto"; 075 076 /** 077 * The directory where the generated <code>plugin.xml</code> file will be put. 078 */ 079 @Parameter(defaultValue = "${project.build.outputDirectory}/META-INF/maven", readonly = true) 080 private File outputDirectory; 081 082 /** 083 * The file encoding of the source files. 084 * 085 * @since 2.5 086 */ 087 @Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}") 088 private String encoding; 089 090 /** 091 * A flag to disable generation of the <code>plugin.xml</code> in favor of a hand authored plugin descriptor. 092 * 093 * @since 2.6 094 */ 095 @Parameter(defaultValue = "false") 096 private boolean skipDescriptor; 097 098 /** 099 * <p> 100 * The role names of mojo extractors to use. 101 * </p> 102 * <p> 103 * If not set, all mojo extractors will be used. If set to an empty extractor name, no mojo extractors 104 * will be used. 105 * </p> 106 * Example: 107 * <pre> 108 * <!-- Use all mojo extractors --> 109 * <extractors/> 110 * 111 * <!-- Use no mojo extractors --> 112 * <extractors> 113 * <extractor/> 114 * </extractors> 115 * 116 * <!-- Use only bsh mojo extractor --> 117 * <extractors> 118 * <extractor>bsh</extractor> 119 * </extractors> 120 * </pre> 121 * The extractors with the following names ship with {@code maven-plugin-tools}: 122 * <ol> 123 * <li>{@code java-annotations}</li> 124 * <li>{@code java-javadoc}, deprecated</li> 125 * <li>{@code ant}, deprecated</li> 126 * <li>{@code bsh}, deprecated</li> 127 * </ol> 128 */ 129 @Parameter 130 private Set<String> extractors; 131 132 /** 133 * By default, an exception is throw if no mojo descriptor is found. As the maven-plugin is defined in core, the 134 * descriptor generator mojo is bound to generate-resources phase. 135 * But for annotations, the compiled classes are needed, so skip error 136 * 137 * @since 3.0 138 */ 139 @Parameter(property = "maven.plugin.skipErrorNoDescriptorsFound", defaultValue = "false") 140 private boolean skipErrorNoDescriptorsFound; 141 142 /** 143 * Flag controlling is "expected dependencies in provided scope" check to be performed or not. Default value: 144 * {@code true}. 145 * 146 * @since 3.6.3 147 */ 148 @Parameter(defaultValue = "true", property = "maven.plugin.checkExpectedProvidedScope") 149 private boolean checkExpectedProvidedScope = true; 150 151 /** 152 * List of {@code groupId} strings of artifact coordinates that are expected to be in "provided" scope. Default 153 * value: {@code ["org.apache.maven"]}. 154 * 155 * @since 3.6.3 156 */ 157 @Parameter 158 private List<String> expectedProvidedScopeGroupIds = Collections.singletonList("org.apache.maven"); 159 160 /** 161 * List of {@code groupId:artifactId} strings of artifact coordinates that are to be excluded from "expected 162 * provided scope" check. Default value: 163 * {@code ["org.apache.maven:maven-archiver", "org.apache.maven:maven-jxr", "org.apache.maven:plexus-utils"]}. 164 * 165 * @since 3.6.3 166 */ 167 @Parameter 168 private List<String> expectedProvidedScopeExclusions = Arrays.asList( 169 "org.apache.maven:maven-archiver", "org.apache.maven:maven-jxr", "org.apache.maven:plexus-utils"); 170 171 /** 172 * Specify the dependencies as {@code groupId:artifactId} containing (abstract) Mojos, to filter 173 * dependencies scanned at runtime and focus on dependencies that are really useful to Mojo analysis. 174 * By default, the value is {@code null} and all dependencies are scanned (as before this parameter was added). 175 * If specified in the configuration with no children, no dependencies are scanned. 176 * 177 * @since 3.5 178 */ 179 @Parameter 180 private List<String> mojoDependencies = null; 181 182 /** 183 * Creates links to existing external javadoc-generated documentation. 184 * <br> 185 * <b>Notes</b>: 186 * all given links should have a fetchable {@code /package-list} or {@code /element-list} file. 187 * For instance: 188 * <pre> 189 * <externalJavadocBaseUrls> 190 * <externalJavadocBaseUrl>https://docs.oracle.com/javase/8/docs/api/</externalJavadocBaseUrl> 191 * <externalJavadocBaseUrls> 192 * </pre> 193 * is valid because <code>https://docs.oracle.com/javase/8/docs/api/package-list</code> exists. 194 * See <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javadoc.html#standard-doclet-options"> 195 * link option of the javadoc tool</a>. 196 * Using this parameter requires connectivity to the given URLs during the goal execution. 197 * @since 3.7.0 198 */ 199 @Parameter(property = "externalJavadocBaseUrls", alias = "links") 200 protected List<URI> externalJavadocBaseUrls; 201 202 /** 203 * The base URL for the Javadoc site containing the current project's API documentation. 204 * This may be relative to the root of the generated Maven site. 205 * It does not need to exist yet at the time when this goal is executed. 206 * Must end with a slash. 207 * <b>In case this is set the javadoc reporting goal should be executed prior to 208 * <a href="../maven-plugin-report-plugin/index.html">Plugin Report</a>.</b> 209 * @since 3.7.0 210 */ 211 @Parameter(property = "internalJavadocBaseUrl") 212 protected URI internalJavadocBaseUrl; 213 214 /** 215 * The version of the javadoc tool (equal to the container JDK version) used to generate the internal javadoc 216 * Only relevant if {@link #internalJavadocBaseUrl} is set. 217 * The default value needs to be overwritten in case toolchains are being used for generating Javadoc. 218 * 219 * @since 3.7.0 220 */ 221 @Parameter(property = "internalJavadocVersion", defaultValue = "${java.version}") 222 protected String internalJavadocVersion; 223 224 /** 225 * The Maven Settings, for evaluating proxy settings used to access {@link #javadocLinks} 226 * 227 * @since 3.7.0 228 */ 229 @Parameter(defaultValue = "${settings}", readonly = true, required = true) 230 private Settings settings; 231 232 @Parameter(defaultValue = "${repositorySystemSession}", readonly = true, required = true) 233 private RepositorySystemSession repoSession; 234 /** 235 * The required Java version to set in the plugin descriptor. This is evaluated by Maven 4 and ignored by earlier 236 * Maven versions. Can be either one of the following formats: 237 * 238 * <ul> 239 * <li>A version range which specifies the supported Java versions. It can either use the usual mathematical 240 * 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 241 * form for {@code "[2.2.1,)"}, i.e. denotes the minimum version required.</li> 242 * <li>{@code "auto"} to determine the minimum Java version from the binary class version being generated during 243 * compilation (determined by the extractor).</li> 244 * </ul> 245 * 246 * @since 3.8.0 247 */ 248 @Parameter(defaultValue = VALUE_AUTO) 249 String requiredJavaVersion; 250 251 /** 252 * The required Maven version to set in the plugin descriptor. This is evaluated by Maven 4 and ignored by earlier 253 * Maven versions. Can be either one of the following formats: 254 * 255 * <ul> 256 * <li>A version range which specifies the supported Maven versions. It can either use the usual mathematical 257 * 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 258 * form for {@code "[2.2.1,)"}, i.e. denotes the minimum version required.</li> 259 * <li>{@code "auto"} to determine the minimum Maven version from the POM's Maven prerequisite, or if not set the 260 * referenced Maven Plugin API version.</li> 261 * </ul> 262 * This value takes precedence over the 263 * <a href="https://maven.apache.org/pom.html#Prerequisites">POM's Maven prerequisite</a> in Maven 4. 264 * 265 * @since 3.8.0 266 */ 267 @Parameter(defaultValue = VALUE_AUTO) 268 String requiredMavenVersion; 269 270 /** 271 * The component used for scanning the source tree for mojos. 272 */ 273 @Component 274 private MojoScanner mojoScanner; 275 276 @Component 277 protected BuildContext buildContext; 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(repoSession); 350 request.setInternalJavadocBaseUrl(internalJavadocBaseUrl); 351 request.setInternalJavadocVersion(internalJavadocVersion); 352 request.setExternalJavadocBaseUrls(externalJavadocBaseUrls); 353 request.setSettings(settings); 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 ExtendedPluginDescriptor extendedPluginDescriptor = new ExtendedPluginDescriptor(request.getPluginDescriptor()); 380 extendedPluginDescriptor.setRequiredJavaVersion(getRequiredJavaVersion(request)); 381 extendedPluginDescriptor.setRequiredMavenVersion(getRequiredMavenVersion(request)); 382 return extendedPluginDescriptor; 383 } 384 385 private String getRequiredMavenVersion(PluginToolsRequest request) { 386 if (!VALUE_AUTO.equals(requiredMavenVersion)) { 387 return requiredMavenVersion; 388 } 389 getLog().debug("Trying to derive Maven version automatically from project prerequisites..."); 390 String requiredMavenVersion = 391 project.getPrerequisites() != null ? project.getPrerequisites().getMaven() : null; 392 if (requiredMavenVersion == null) { 393 getLog().debug("Trying to derive Maven version automatically from referenced Maven Plugin API artifact " 394 + "version..."); 395 requiredMavenVersion = request.getUsedMavenApiVersion(); 396 } 397 if (requiredMavenVersion == null) { 398 getLog().warn("Cannot determine the required Maven version automatically, it is recommended to " 399 + "configure some explicit value manually."); 400 } 401 return requiredMavenVersion; 402 } 403 404 private String getRequiredJavaVersion(PluginToolsRequest request) { 405 if (!VALUE_AUTO.equals(requiredJavaVersion)) { 406 return requiredJavaVersion; 407 } 408 String minRequiredJavaVersion = request.getRequiredJavaVersion(); 409 if (minRequiredJavaVersion == null) { 410 getLog().warn("Cannot determine the minimally required Java version automatically, it is recommended to " 411 + "configure some explicit value manually."); 412 return null; 413 } 414 415 return minRequiredJavaVersion; 416 } 417 418 /** 419 * Collects all dependencies expected to be in "provided" scope but are NOT in "provided" scope. 420 */ 421 private Set<Artifact> dependenciesNotInProvidedScope() { 422 LinkedHashSet<Artifact> wrongScopedDependencies = new LinkedHashSet<>(); 423 424 for (Artifact dependency : project.getArtifacts()) { 425 String ga = dependency.getGroupId() + ":" + dependency.getArtifactId(); 426 if (expectedProvidedScopeGroupIds.contains(dependency.getGroupId()) 427 && !expectedProvidedScopeExclusions.contains(ga) 428 && !Artifact.SCOPE_PROVIDED.equals(dependency.getScope())) { 429 wrongScopedDependencies.add(dependency); 430 } 431 } 432 433 return wrongScopedDependencies; 434 } 435 436 /** 437 * Get dependencies filtered with mojoDependencies configuration. 438 * 439 * @return eventually filtered dependencies, or even <code>null</code> if configured with empty mojoDependencies 440 * list 441 * @see #mojoDependencies 442 */ 443 private Set<Artifact> filterMojoDependencies() { 444 Set<Artifact> filteredArtifacts; 445 if (mojoDependencies == null) { 446 filteredArtifacts = new LinkedHashSet<>(project.getArtifacts()); 447 } else if (mojoDependencies.isEmpty()) { 448 filteredArtifacts = null; 449 } else { 450 filteredArtifacts = new LinkedHashSet<>(); 451 452 ArtifactFilter filter = new IncludesArtifactFilter(mojoDependencies); 453 454 for (Artifact artifact : project.getArtifacts()) { 455 if (filter.include(artifact)) { 456 filteredArtifacts.add(artifact); 457 } 458 } 459 } 460 461 return filteredArtifacts; 462 } 463}