diff --git a/src/main/java/org/apache/maven/buildcache/BuildCacheMojosExecutionStrategy.java b/src/main/java/org/apache/maven/buildcache/BuildCacheMojosExecutionStrategy.java index 63efca75..e9d0859d 100644 --- a/src/main/java/org/apache/maven/buildcache/BuildCacheMojosExecutionStrategy.java +++ b/src/main/java/org/apache/maven/buildcache/BuildCacheMojosExecutionStrategy.java @@ -503,6 +503,14 @@ boolean isParamsMatched( CompletedExecution completedExecution) { List tracked = cacheConfig.getTrackedProperties(mojoExecution); + if (mojoExecution.getPlugin() != null) { + LOGGER.debug( + "Checking parameter match for {}:{} - tracking {} properties", + mojoExecution.getPlugin().getArtifactId(), + mojoExecution.getGoal(), + tracked.size()); + } + for (TrackedProperty trackedProperty : tracked) { final String propertyName = trackedProperty.getPropertyName(); @@ -511,7 +519,7 @@ boolean isParamsMatched( expectedValue = trackedProperty.getDefaultValue() != null ? trackedProperty.getDefaultValue() : "null"; } - final String currentValue; + String currentValue; try { Object value; if (trackedProperty.getExpression() != null) { @@ -524,8 +532,14 @@ boolean isParamsMatched( } catch (IllegalAccessException e) { LOGGER.error("Cannot extract plugin property {} from mojo {}", propertyName, mojo, e); return false; + } catch (Exception e) { + LOGGER.warn("Cannot extract plugin property {} from mojo {}", propertyName, mojo, e); + return false; } + LOGGER.debug( + "Checking property '{}': expected='{}', actual='{}'", propertyName, expectedValue, currentValue); + if (!Strings.CS.equals(currentValue, expectedValue)) { if (!Strings.CS.equals(currentValue, trackedProperty.getSkipValue())) { LOGGER.info( diff --git a/src/main/java/org/apache/maven/buildcache/CacheControllerImpl.java b/src/main/java/org/apache/maven/buildcache/CacheControllerImpl.java index 746034c8..c514c874 100644 --- a/src/main/java/org/apache/maven/buildcache/CacheControllerImpl.java +++ b/src/main/java/org/apache/maven/buildcache/CacheControllerImpl.java @@ -943,7 +943,11 @@ private void recordMojoProperties(CompletedExecution execution, MojoExecutionEve try { Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(propertyName, mojo.getClass()); if (field != null) { - final Object value = ReflectionUtils.getValueIncludingSuperclasses(propertyName, mojo); + final Object value = normalizeMojoProperty( + propertyName, + ReflectionUtils.getValueIncludingSuperclasses(propertyName, mojo), + mojoExecution, + executionEvent.getProject()); CacheUtils.addProperty(execution, propertyName, value, baseDirPath, tracked); continue; } @@ -985,6 +989,30 @@ private void recordMojoProperties(CompletedExecution execution, MojoExecutionEve } } + private Object normalizeMojoProperty( + String propertyName, Object value, MojoExecution mojoExecution, MavenProject project) { + if (!(value instanceof List) + || !"compilerArgs".equals(propertyName) + || mojoExecution.getPlugin() == null + || !"maven-compiler-plugin".equals(mojoExecution.getPlugin().getArtifactId())) { + return value; + } + + List args = (List) value; + List normalized = new ArrayList<>(args.size()); + for (int i = 0; i < args.size(); i++) { + Object arg = args.get(i); + if ("--module-version".equals(arg) + && i + 1 < args.size() + && Objects.equals(project.getVersion(), args.get(i + 1))) { + i++; + } else { + normalized.add(arg); + } + } + return normalized.isEmpty() ? null : normalized; + } + private static Method getGetter(String fieldName, Class clazz) { String getterMethodName = "get" + org.codehaus.plexus.util.StringUtils.capitalizeFirstLetter(fieldName); Method[] methods = clazz.getMethods(); diff --git a/src/main/java/org/apache/maven/buildcache/xml/CacheConfigImpl.java b/src/main/java/org/apache/maven/buildcache/xml/CacheConfigImpl.java index a3e04dfe..662588a8 100644 --- a/src/main/java/org/apache/maven/buildcache/xml/CacheConfigImpl.java +++ b/src/main/java/org/apache/maven/buildcache/xml/CacheConfigImpl.java @@ -48,7 +48,6 @@ import org.apache.maven.buildcache.xml.config.Exclude; import org.apache.maven.buildcache.xml.config.Executables; import org.apache.maven.buildcache.xml.config.ExecutionConfigurationScan; -import org.apache.maven.buildcache.xml.config.ExecutionControl; import org.apache.maven.buildcache.xml.config.ExecutionIdsList; import org.apache.maven.buildcache.xml.config.GoalReconciliation; import org.apache.maven.buildcache.xml.config.GoalsList; @@ -122,6 +121,7 @@ public class CacheConfigImpl implements org.apache.maven.buildcache.xml.CacheCon private final XmlService xmlService; private final Provider providerSession; private final RuntimeInformation rtInfo; + private final PluginParameterLoader parameterLoader; private volatile CacheState state; private CacheConfig cacheConfig; @@ -133,6 +133,7 @@ public CacheConfigImpl(XmlService xmlService, Provider providerSes this.xmlService = xmlService; this.providerSession = providerSession; this.rtInfo = rtInfo; + this.parameterLoader = new PluginParameterLoader(); } @Nonnull @@ -252,27 +253,181 @@ public boolean isLogAllProperties(MojoExecution mojoExecution) { } private GoalReconciliation findReconciliationConfig(MojoExecution mojoExecution) { - if (cacheConfig.getExecutionControl() == null) { + if (mojoExecution == null) { return null; } - final ExecutionControl executionControl = cacheConfig.getExecutionControl(); - if (executionControl.getReconcile() == null) { + final String goal = mojoExecution.getGoal(); + final Plugin plugin = mojoExecution.getPlugin(); + + if (plugin == null) { return null; } - final List reconciliation = - executionControl.getReconcile().getPlugins(); + // First check explicit configuration + if (cacheConfig.getExecutionControl() != null + && cacheConfig.getExecutionControl().getReconcile() != null) { + List explicitConfigs = + cacheConfig.getExecutionControl().getReconcile().getPlugins(); + for (GoalReconciliation config : explicitConfigs) { + if (isPluginMatch(plugin, config) && Strings.CS.equals(goal, config.getGoal())) { + // Validate explicit config against parameter definitions (with version) + validateReconciliationConfig(config, plugin); + return config; + } + } + } - for (GoalReconciliation goalReconciliationConfig : reconciliation) { - final String goal = mojoExecution.getGoal(); + // Auto-generate from parameter definitions (track all cache-key parameters) + GoalReconciliation autoGenerated = generateReconciliationFromParameters(plugin, goal); + if (autoGenerated != null) { + LOGGER.debug( + "Auto-generated reconciliation config for {}:{} with {} cache-key parameters", + plugin.getArtifactId(), + goal, + autoGenerated.getReconciles() != null + ? autoGenerated.getReconciles().size() + : 0); + } + return autoGenerated; + } - if (isPluginMatch(mojoExecution.getPlugin(), goalReconciliationConfig) - && Strings.CS.equals(goal, goalReconciliationConfig.getGoal())) { - return goalReconciliationConfig; + /** + * Validates a single reconciliation config against plugin parameter definitions. + * Uses plugin version to load the appropriate parameter definition. + */ + private void validateReconciliationConfig(GoalReconciliation config, Plugin plugin) { + String artifactId = config.getArtifactId(); + String goal = config.getGoal(); + String pluginVersion = plugin.getVersion(); + + // Load parameter definition for this plugin with version + PluginParameterDefinition pluginDef = parameterLoader.load(artifactId, pluginVersion); + + if (pluginDef == null) { + LOGGER.warn( + "No parameter definition found for plugin {}:{} version {}. " + + "Cannot validate reconciliation configuration. " + + "Consider adding a parameter definition file to plugin-parameters/{}.xml", + artifactId, + goal, + pluginVersion != null ? pluginVersion : "unknown", + artifactId); + return; + } + + // Get goal definition + PluginParameterDefinition.GoalParameterDefinition goalDef = pluginDef.getGoal(goal); + if (goalDef == null) { + LOGGER.warn( + "Goal '{}' not found in parameter definition for plugin {} version {}. " + + "Cannot validate reconciliation configuration.", + goal, + artifactId, + pluginVersion != null ? pluginVersion : "unknown"); + return; + } + + // Validate each tracked property + List properties = config.getReconciles(); + + for (TrackedProperty property : properties) { + String propertyName = property.getPropertyName(); + + if (!goalDef.hasParameter(propertyName)) { + LOGGER.warn( + "Unknown parameter '{}' in reconciliation config for {}:{} version {}. " + + "This may indicate a plugin version mismatch or renamed parameter. " + + "Consider updating parameter definition or removing from reconciliation.", + propertyName, + artifactId, + goal, + pluginVersion != null ? pluginVersion : "unknown"); + } else { + PluginParameterDefinition.ParameterDefinition paramDef = goalDef.getParameter(propertyName); + if (!paramDef.isCacheKey()) { + LOGGER.warn( + "Parameter '{}' in reconciliation config for {}:{} is not marked as a cache key. " + + "Changing it will not invalidate the cache according to the plugin definition.", + propertyName, + artifactId, + goal); + } } } - return null; + } + + /** + * Auto-generates a reconciliation config by tracking all cache-key parameters + * from the plugin parameter definition. + * This provides automatic default tracking for any plugin with parameter definitions. + * + * @param plugin The plugin to generate config for + * @param goal The goal name + * @return Auto-generated config tracking all cache-key parameters, or null if no parameter definition exists + */ + private GoalReconciliation generateReconciliationFromParameters(Plugin plugin, String goal) { + String artifactId = plugin.getArtifactId(); + String pluginVersion = plugin.getVersion(); + + LOGGER.debug( + "Attempting to auto-generate reconciliation config for {}:{} version {}", + artifactId, + goal, + pluginVersion); + + // Load parameter definition for this plugin + PluginParameterDefinition pluginDef = parameterLoader.load(artifactId, pluginVersion); + if (pluginDef == null) { + LOGGER.debug("No parameter definition found for {}:{}", artifactId, pluginVersion); + return null; + } + + // Get goal definition + PluginParameterDefinition.GoalParameterDefinition goalDef = pluginDef.getGoal(goal); + if (goalDef == null) { + LOGGER.debug("No goal definition found for goal '{}' in plugin {}", goal, artifactId); + return null; + } + + LOGGER.debug( + "Found goal definition for {}:{} with {} total parameters", + artifactId, + goal, + goalDef.getParameters().size()); + + // Collect all cache-key parameters + List cacheKeyProperties = new ArrayList<>(); + for (PluginParameterDefinition.ParameterDefinition param : + goalDef.getParameters().values()) { + if (param.isCacheKey()) { + LOGGER.debug("Adding cache-key parameter '{}' to auto-generated config", param.getName()); + TrackedProperty property = new TrackedProperty(); + property.setPropertyName(param.getName()); + cacheKeyProperties.add(property); + } + } + + // Only create config if there are cache-key parameters to track + if (cacheKeyProperties.isEmpty()) { + LOGGER.debug("No cache-key parameters found for {}:{}", artifactId, goal); + return null; + } + + LOGGER.debug("Created auto-generated config with {} cache-key parameters", cacheKeyProperties.size()); + + // Create auto-generated reconciliation config + GoalReconciliation config = new GoalReconciliation(); + config.setArtifactId(artifactId); + if (plugin.getGroupId() != null) { + config.setGroupId(plugin.getGroupId()); + } + config.setGoal(goal); + for (TrackedProperty property : cacheKeyProperties) { + config.addReconcile(property); + } + + return config; } @Nonnull diff --git a/src/main/java/org/apache/maven/buildcache/xml/PluginParameterDefinition.java b/src/main/java/org/apache/maven/buildcache/xml/PluginParameterDefinition.java new file mode 100644 index 00000000..c7d9da40 --- /dev/null +++ b/src/main/java/org/apache/maven/buildcache/xml/PluginParameterDefinition.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.buildcache.xml; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents the complete parameter definition for a Maven plugin loaded from XML. + * Contains all goals and their parameters with cache-key metadata. + */ +public class PluginParameterDefinition { + + private final String groupId; + private final String artifactId; + private final String minVersion; + private final Map goals; + + public PluginParameterDefinition(String groupId, String artifactId, String minVersion) { + this.groupId = groupId; + this.artifactId = artifactId; + this.minVersion = minVersion; + this.goals = new HashMap<>(); + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public String getMinVersion() { + return minVersion; + } + + public void addGoal(String goalName, GoalParameterDefinition goal) { + goals.put(goalName, goal); + } + + public GoalParameterDefinition getGoal(String goalName) { + return goals.get(goalName); + } + + public Map getGoals() { + return goals; + } + + /** + * Represents parameters for a single goal + */ + public static class GoalParameterDefinition { + private final String name; + private final Map parameters; + + public GoalParameterDefinition(String name) { + this.name = name; + this.parameters = new HashMap<>(); + } + + public String getName() { + return name; + } + + public void addParameter(ParameterDefinition parameter) { + parameters.put(parameter.getName(), parameter); + } + + public ParameterDefinition getParameter(String paramName) { + return parameters.get(paramName); + } + + public Map getParameters() { + return parameters; + } + + public boolean hasParameter(String paramName) { + return parameters.containsKey(paramName); + } + } + + /** + * Represents a single parameter definition + */ + public static class ParameterDefinition { + private final String name; + private final boolean cacheKey; + private final String description; + + public ParameterDefinition(String name, boolean cacheKey, String description) { + this.name = name; + this.cacheKey = cacheKey; + this.description = description; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + /** + * Whether changing this parameter must cause a cache miss. + */ + public boolean isCacheKey() { + return cacheKey; + } + } +} diff --git a/src/main/java/org/apache/maven/buildcache/xml/PluginParameterLoader.java b/src/main/java/org/apache/maven/buildcache/xml/PluginParameterLoader.java new file mode 100644 index 00000000..133ddc3b --- /dev/null +++ b/src/main/java/org/apache/maven/buildcache/xml/PluginParameterLoader.java @@ -0,0 +1,278 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.buildcache.xml; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import java.io.InputStream; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.maven.buildcache.xml.PluginParameterDefinition.GoalParameterDefinition; +import org.apache.maven.buildcache.xml.PluginParameterDefinition.ParameterDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +/** + * Loads plugin parameter definitions from classpath resources. + * Definitions are stored in src/main/resources/plugin-parameters/{artifactId}.xml + */ +public class PluginParameterLoader { + + private static final Logger LOGGER = LoggerFactory.getLogger(PluginParameterLoader.class); + private static final String PARAMETER_DIR = "plugin-parameters/"; + + private final Map definitions = new ConcurrentHashMap<>(); + + /** + * Load parameter definitions for a plugin by artifact ID only (no version matching) + */ + public PluginParameterDefinition load(String artifactId) { + return load(artifactId, null); + } + + /** + * Load parameter definitions for a plugin by artifact ID and version. + * If version is provided, finds the best matching definition (highest minVersion <= actual version). + * If version is null, returns any definition for the artifactId. + */ + public PluginParameterDefinition load(String artifactId, String pluginVersion) { + String cacheKey = artifactId + (pluginVersion != null ? ":" + pluginVersion : ""); + + if (definitions.containsKey(cacheKey)) { + return definitions.get(cacheKey); + } + + String resourcePath = PARAMETER_DIR + artifactId + ".xml"; + InputStream is = getClass().getClassLoader().getResourceAsStream(resourcePath); + + if (is == null) { + LOGGER.debug("No parameter definition found for plugin: {}", artifactId); + return null; + } + + try (InputStream inputStream = is) { + java.util.List allDefinitions = parseDefinitions(inputStream, artifactId); + + PluginParameterDefinition bestMatch = findBestMatch(allDefinitions, pluginVersion); + + if (bestMatch != null) { + definitions.put(cacheKey, bestMatch); + LOGGER.debug( + "Loaded parameter definition for {}:{} (minVersion: {}): {} goals, {} total parameters", + artifactId, + pluginVersion != null ? pluginVersion : "any", + bestMatch.getMinVersion() != null ? bestMatch.getMinVersion() : "none", + bestMatch.getGoals().size(), + bestMatch.getGoals().values().stream() + .mapToInt(g -> g.getParameters().size()) + .sum()); + } + + return bestMatch; + } catch (Exception e) { + LOGGER.warn("Failed to load parameter definition for {}: {}", artifactId, e.getMessage(), e); + return null; + } + } + + /** + * Find the best matching definition for a plugin version. + * Returns the definition with the highest minVersion that is <= pluginVersion. + * If pluginVersion is null, returns the first definition (or the one without minVersion). + */ + private PluginParameterDefinition findBestMatch( + java.util.List definitions, String pluginVersion) { + if (definitions.isEmpty()) { + return null; + } + + if (pluginVersion == null) { + // No version specified, prefer definition without minVersion, otherwise return first + return definitions.stream() + .filter(d -> d.getMinVersion() == null) + .findFirst() + .orElse(definitions.get(0)); + } + + // Find highest minVersion that's <= pluginVersion + PluginParameterDefinition bestMatch = null; + String bestMinVersion = null; + + for (PluginParameterDefinition def : definitions) { + String minVersion = def.getMinVersion(); + + // Definition without minVersion applies to all versions + if (minVersion == null) { + if (bestMatch == null) { + bestMatch = def; + } + continue; + } + + // Check if this definition applies to the plugin version + if (compareVersions(minVersion, pluginVersion) <= 0) { + // minVersion <= pluginVersion, so this definition applies + if (bestMinVersion == null || compareVersions(minVersion, bestMinVersion) > 0) { + // This is a better match (higher minVersion) + bestMatch = def; + bestMinVersion = minVersion; + } + } + } + + return bestMatch; + } + + /** + * Compare two version strings. + * Returns: negative if v1 < v2, zero if v1 == v2, positive if v1 > v2 + */ + private int compareVersions(String v1, String v2) { + String[] parts1 = v1.split("\\."); + String[] parts2 = v2.split("\\."); + + int maxLength = Math.max(parts1.length, parts2.length); + + for (int i = 0; i < maxLength; i++) { + int num1 = i < parts1.length ? parseVersionPart(parts1[i]) : 0; + int num2 = i < parts2.length ? parseVersionPart(parts2[i]) : 0; + + if (num1 != num2) { + return Integer.compare(num1, num2); + } + } + + return 0; + } + + private int parseVersionPart(String part) { + try { + // Handle qualifiers like "3.8.0-SNAPSHOT" - just use numeric part + int dashIndex = part.indexOf('-'); + if (dashIndex > 0) { + part = part.substring(0, dashIndex); + } + return Integer.parseInt(part); + } catch (NumberFormatException e) { + return 0; + } + } + + /** + * Parse plugin parameter definitions from XML. + * Supports multiple elements in a single file for version-specific definitions. + */ + private java.util.List parseDefinitions(InputStream is, String artifactId) + throws Exception { + java.util.List definitions = new java.util.ArrayList<>(); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(is); + + Element root = doc.getDocumentElement(); + + // Check if root is a single or if we need to look for multiple + if ("plugin".equals(root.getLocalName())) { + // Single plugin definition + definitions.add(parsePluginDefinition(root)); + } else { + // Look for multiple elements + NodeList pluginNodes = root.getElementsByTagNameNS("*", "plugin"); + for (int i = 0; i < pluginNodes.getLength(); i++) { + Element pluginElement = (Element) pluginNodes.item(i); + definitions.add(parsePluginDefinition(pluginElement)); + } + } + + return definitions; + } + + private PluginParameterDefinition parsePluginDefinition(Element pluginElement) { + String groupId = getTextContent(pluginElement, "groupId"); + String actualArtifactId = getTextContent(pluginElement, "artifactId"); + String minVersion = getTextContent(pluginElement, "minVersion"); + + PluginParameterDefinition definition = new PluginParameterDefinition(groupId, actualArtifactId, minVersion); + + NodeList goalsNodes = pluginElement.getElementsByTagNameNS("*", "goals"); + if (goalsNodes.getLength() > 0) { + Element goalsElement = (Element) goalsNodes.item(0); + NodeList goalNodes = goalsElement.getElementsByTagNameNS("*", "goal"); + + for (int i = 0; i < goalNodes.getLength(); i++) { + Element goalElement = (Element) goalNodes.item(i); + parseGoal(goalElement, definition); + } + } + + return definition; + } + + private void parseGoal(Element goalElement, PluginParameterDefinition definition) { + String goalName = getTextContent(goalElement, "name"); + GoalParameterDefinition goal = new GoalParameterDefinition(goalName); + + NodeList parametersNodes = goalElement.getElementsByTagNameNS("*", "parameters"); + if (parametersNodes.getLength() > 0) { + Element parametersElement = (Element) parametersNodes.item(0); + NodeList parameterNodes = parametersElement.getElementsByTagNameNS("*", "parameter"); + + for (int i = 0; i < parameterNodes.getLength(); i++) { + Element paramElement = (Element) parameterNodes.item(i); + ParameterDefinition param = parseParameter(paramElement); + goal.addParameter(param); + } + } + + definition.addGoal(goalName, goal); + } + + private ParameterDefinition parseParameter(Element paramElement) { + String name = getTextContent(paramElement, "name"); + boolean cacheKey = Boolean.parseBoolean(getTextContent(paramElement, "cache-key")); + String description = getTextContent(paramElement, "description"); + + return new ParameterDefinition(name, cacheKey, description); + } + + private String getTextContent(Element parent, String tagName) { + NodeList nodes = parent.getElementsByTagNameNS("*", tagName); + if (nodes.getLength() > 0) { + return nodes.item(0).getTextContent().trim(); + } + return null; + } +} diff --git a/src/main/resources/default-reconciliation/default-reconciliation.xsd b/src/main/resources/default-reconciliation/default-reconciliation.xsd new file mode 100644 index 00000000..bb93a583 --- /dev/null +++ b/src/main/resources/default-reconciliation/default-reconciliation.xsd @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/plugin-parameters/maven-compiler-plugin.xml b/src/main/resources/plugin-parameters/maven-compiler-plugin.xml new file mode 100644 index 00000000..7872781f --- /dev/null +++ b/src/main/resources/plugin-parameters/maven-compiler-plugin.xml @@ -0,0 +1,318 @@ + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + compile + + + + source + true + Source JDK version for compilation; changing it can change generated bytecode or compilation success + + + target + true + Target JDK version for compiled bytecode; changing it changes generated bytecode + + + release + true + JDK release version (combines source and target); changing it changes generated bytecode and API compatibility + + + encoding + true + Source file encoding; changing it can change parsed source and generated bytecode + + + debug + true + Include debugging information in compiled bytecode; changing it changes artifact contents + + + debuglevel + true + Level of debugging information (lines, vars, source); changing it changes artifact contents + + + optimize + true + Optimize compiled bytecode; changing it changes artifact contents + + + compilerArgs + true + Additional compiler arguments; changing them can change generated bytecode or compilation success + + + compilerArgument + true + Single additional compiler argument; changing it can change generated bytecode or compilation success + + + annotationProcessorPaths + true + Classpath for annotation processors; changing it can change generated sources or compilation success + + + annotationProcessors + true + Annotation processors to run; changing them can change generated sources or compilation success + + + proc + true + Annotation processing mode (none, only, proc); changing it can change generated sources or compilation success + + + executable + true + Path to javac executable; changing it can change the compiler output or compilation success + + + parameters + true + Generate metadata for method parameters; changing it changes bytecode contents + + + enablePreview + true + Enable preview features; changing it can change generated bytecode or compilation success + + + + + verbose + Verbose compiler output + + + showWarnings + true + Show compilation warnings (can change the build outcome with failOnWarning) + + + showDeprecation + true + Show deprecation warnings (can change the build outcome with failOnWarning) + + + skipMain + true + Skip compiling main sources; changing it changes whether main bytecode is produced + + + failOnError + true + Fail build on compilation error (changes the build outcome) + + + failOnWarning + true + Fail build on compilation warning (changes the build outcome) + + + fork + true + Fork compiler into separate process (can change the compiler, output, or outcome) + + + maxmem + true + Maximum memory for compiler process (can change the build outcome) + + + meminitial + true + Initial memory for compiler process (can change the build outcome) + + + compilerReuseStrategy + Strategy for reusing compiler instances + + + forceJavacCompilerUse + true + Force use of javac compiler; changing it can affect compiler output + + + staleMillis + true + Staleness check interval for incremental compilation (can change generated files) + + + useIncrementalCompilation + true + Enable incremental compilation (can change generated files and build outcome) + + + + + + testCompile + + + + source + true + Source JDK version for test compilation; changing it can change generated test bytecode or compilation success + + + target + true + Target JDK version for compiled test bytecode; changing it changes generated test bytecode + + + release + true + JDK release version for tests; changing it changes generated test bytecode and API compatibility + + + encoding + true + Test source file encoding; changing it can change parsed source and generated test bytecode + + + debug + true + Include debugging information in test bytecode; changing it changes artifact contents + + + debuglevel + true + Level of debugging information for tests; changing it changes artifact contents + + + optimize + true + Optimize test bytecode; changing it changes artifact contents + + + compilerArgs + true + Additional compiler arguments; changing them can change generated bytecode or compilation success for tests + + + compilerArgument + true + Single additional compiler argument; changing it can change generated bytecode or compilation success for tests + + + annotationProcessorPaths + true + Classpath for test annotation processors; changing it can change generated test sources or compilation success + + + annotationProcessors + true + Annotation processors for tests; changing them can change generated test sources or compilation success + + + proc + true + Annotation processing mode for tests; changing it can change generated test sources or compilation success + + + executable + true + Path to javac for test compilation; changing it can change compiler output or compilation success + + + parameters + true + Generate metadata for test method parameters; changing it changes bytecode contents + + + enablePreview + true + Enable preview features for tests; changing it can change generated bytecode or compilation success + + + verbose + Verbose test compiler output + + + showWarnings + true + Show test compilation warnings (can change the build outcome with failOnWarning) + + + showDeprecation + true + Show test deprecation warnings (can change the build outcome with failOnWarning) + + + skip + true + Skip test compilation; changing it changes whether test bytecode is produced + + + failOnError + true + Fail build on test compilation error (changes the build outcome) + + + failOnWarning + true + Fail build on test compilation warning (changes the build outcome) + + + fork + true + Fork test compiler process (can change the compiler, output, or outcome) + + + maxmem + true + Maximum memory for test compiler (can change the build outcome) + + + meminitial + true + Initial memory for test compiler (can change the build outcome) + + + compilerReuseStrategy + Compiler reuse strategy for tests + + + forceJavacCompilerUse + true + Force javac for test compilation; changing it can affect compiler output + + + staleMillis + true + Staleness check for test incremental compilation (can change generated files) + + + useIncrementalCompilation + true + Enable incremental test compilation (can change generated files and build outcome) + + + + + diff --git a/src/main/resources/plugin-parameters/maven-install-plugin.xml b/src/main/resources/plugin-parameters/maven-install-plugin.xml new file mode 100644 index 00000000..918f87cd --- /dev/null +++ b/src/main/resources/plugin-parameters/maven-install-plugin.xml @@ -0,0 +1,107 @@ + + + + org.apache.maven.plugins + maven-install-plugin + 3.1.1 + + + + install + + + + skip + true + Skip installation; changing it changes installed artifacts and local repository state + + + + + installAtEnd + true + Install artifacts at end of multi-module build (changes installed project state on failure) + + + + + + install-file + + + + file + true + The file to install; changing it changes the installed artifact contents + + + groupId + true + GroupId of the artifact; changing it changes the installed artifact coordinates + + + artifactId + true + ArtifactId of the artifact; changing it changes the installed artifact coordinates + + + version + true + Version of the artifact; changing it changes the installed artifact coordinates + + + packaging + true + Packaging type of the artifact; changing it changes the installed artifact format + + + classifier + true + Classifier of the artifact; changing it changes the installed artifact coordinates + + + pomFile + true + POM file to install; changing it changes installed metadata and project state + + + generatePom + true + Generate a minimal POM; changing it changes installed metadata + + + javadoc + true + Javadoc artifact to install; changing it changes installed artifacts + + + localRepositoryPath + true + Local repository path; changing it changes where installed artifacts are written + + + sources + true + Sources artifact to install; changing it changes installed artifacts + + + + + diff --git a/src/main/resources/plugin-parameters/plugin-parameters.xsd b/src/main/resources/plugin-parameters/plugin-parameters.xsd new file mode 100644 index 00000000..33ae6e88 --- /dev/null +++ b/src/main/resources/plugin-parameters/plugin-parameters.xsd @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/site/markdown/concepts.md b/src/site/markdown/concepts.md index 40a765c0..16d4fa56 100644 --- a/src/site/markdown/concepts.md +++ b/src/site/markdown/concepts.md @@ -81,15 +81,21 @@ processing rules in an XML file. To maximize correctness: * Select every relevant file as input to the engine -* Add all the functional plugin parameters to the reconciliation +* Add all plugin parameters marked `true` to the reconciliation To maximize reuse you need to: * Filter out non-essential files (documentation, IDE configs, and similar) -* Minimize the overall number of controlled plugin parameters and exclude behavioral plugin parameters (like the number +* Minimize the overall number of controlled plugin parameters and exclude non-cache-key plugin parameters (like the number of threads or log level) * Make source code relocatable (environment agnostic) +**Note:** The build cache extension includes a parameter validation system that uses the `` annotation to +identify properties that must invalidate the cache when their values change. The annotation defaults to `false`. +The system validates reconciliation configurations and warns about unknown or non-cache-key parameters. See the +[Parameter Validation section](how-to.html#Parameter_Validation_and_Categorization) for details on adding definitions +for new plugins. + Effectively, cache setup involves inspecting the build, taking these decisions, and reflecting them in the cache configuration. diff --git a/src/site/markdown/how-to.md b/src/site/markdown/how-to.md index 2c1d3be4..59292ebd 100644 --- a/src/site/markdown/how-to.md +++ b/src/site/markdown/how-to.md @@ -169,6 +169,177 @@ Add `executionControl/runAlways` section: ``` +### Default Reconciliation Behavior + +The build cache extension automatically tracks certain critical plugin properties by default, even without explicit +`executionControl` configuration. These defaults are derived from plugin parameter descriptors under +`plugin-parameters/`: + +* **maven-compiler-plugin** (`compile` and `testCompile` goals): Tracks parameters marked `true` in + `plugin-parameters/maven-compiler-plugin.xml` +* **maven-install-plugin** (`install` and `install-file` goals): Tracks parameters marked `true` in + `plugin-parameters/maven-install-plugin.xml` + +This default behavior prevents common cache invalidation issues, particularly in multi-module JPMS (Java Platform Module System) +projects where compiler version changes can cause compilation failures. + +**Overriding Defaults:** When you explicitly configure `executionControl` for a plugin goal, your explicit configuration +overrides the built-in defaults for that matching plugin and goal. Other goals continue to use their defaults. For example, +to track only the `release` property for the `compile` goal of maven-compiler-plugin instead of the cache-key parameters +from `plugin-parameters/maven-compiler-plugin.xml`: + +```xml + + + ... + + + + + + + + + + + + + +``` + +This configuration in your `.mvn/maven-build-cache-config.xml` file replaces the built-in defaults. You can also define +reconciliation configurations for plugins that don't have built-in defaults using the same syntax. + +### Parameter Validation and Categorization + +The build cache extension includes a parameter validation system that categorizes plugin parameters and validates +reconciliation configurations against known parameter definitions. + +#### Parameter Categories + +Each parameter definition may contain an optional `` annotation: + +* `true` means that changing the property's value must cause a cache miss. +* `false` or an omitted annotation means that changing the property's value does not invalidate + the cache. The default is `false`. + +The annotation is deliberately about cache identity, not how a plugin executes. Set it to `true` when the property can +change generated files, installed artifacts, project state, artifact contents, or the build success/failure outcome. +Leave it `false` when the property only changes execution policy or diagnostics without changing those observable results. + +#### Validation Features + +The extension automatically validates reconciliation configurations and logs warnings/errors for: + +* **Unknown parameters**: Parameters not defined in the plugin's parameter definition (WARN level) + - May indicate a plugin version mismatch or renamed parameter + - Suggests updating parameter definitions or removing the parameter from reconciliation + +* **Non-cache-key parameters in reconciliation**: Parameters without `true` (WARN level) + - Indicates that the parameter is not defined to invalidate the cache + - Consider removing it or marking it as a cache key if changing it affects outputs or build success/failure + +#### Adding Parameter Definitions for New Plugins + +Parameter definitions are stored in `src/main/resources/plugin-parameters/{artifactId}.xml`. To add validation for a new plugin: + +1. Create an XML file following the schema in `plugin-parameters.xsd`: + +```xml + + + org.apache.maven.plugins + maven-example-plugin + + + + example-goal + + + outputDirectory + true + Directory where output is written; changing it changes generated-file locations + + + verbose + Enable verbose logging; omitted cache-key defaults to false + + + + + +``` + +2. Place the file in the classpath at `plugin-parameters/{artifactId}.xml` + +3. The extension will automatically load and validate against this definition + +#### Version-Specific Parameter Definitions + +The parameter validation system supports version-specific definitions to handle plugins that change parameters across versions. This allows accurate validation even when plugin APIs evolve. + +**How Version Matching Works:** + +- Definitions include a `minVersion` element specifying the minimum plugin version they apply to +- At runtime, the extension selects the definition with the highest `minVersion` that is ≤ the actual plugin version +- Multiple version-specific definitions can exist in a single file + +**Example with version-specific parameters:** + +```xml + + + + + org.apache.maven.plugins + maven-example-plugin + 1.0.0 + + + process + + + legacyParameter + true + Deprecated in 3.0.0; changing it changes the generated result + + + + + + + + + org.apache.maven.plugins + maven-example-plugin + 3.0.0 + + + process + + + newParameter + true + Added in 3.0.0; changing it changes the generated result + + + + + + +``` + +**Version Selection Examples:** + +- Plugin version `1.5.0` → Uses definition with `minVersion=1.0.0` +- Plugin version `3.0.0` → Uses definition with `minVersion=3.0.0` +- Plugin version `4.0.0` → Uses definition with `minVersion=3.0.0` (highest available) +- SNAPSHOT versions are handled correctly (e.g., `3.0.0-SNAPSHOT` matches `minVersion=3.0.0`) + +**Current Coverage**: Parameter definitions are included for `maven-compiler-plugin` and `maven-install-plugin`. + ### I occasionally cached build with `-DskipTests=true`, and tests do not run now If you add command line flags to your build, they do not participate in effective pom - Maven defers the final value diff --git a/src/test/java/org/apache/maven/buildcache/its/DefaultReconciliationTest.java b/src/test/java/org/apache/maven/buildcache/its/DefaultReconciliationTest.java new file mode 100644 index 00000000..2684e81e --- /dev/null +++ b/src/test/java/org/apache/maven/buildcache/its/DefaultReconciliationTest.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.buildcache.its; + +import java.io.IOException; +import java.util.Arrays; + +import org.apache.maven.buildcache.its.junit.IntegrationTest; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.junit.jupiter.api.Test; + +/** + * Test that default reconciliation configs are applied when no executionControl is configured. + * Verifies that compiler properties (source, target, release) are tracked by default. + */ +@IntegrationTest("src/test/projects/default-reconciliation") +class DefaultReconciliationTest { + + @Test + void testDefaultReconciliationWithNoConfig(Verifier verifier) throws VerificationException, IOException { + verifier.setAutoclean(false); + + // First build with release=17 + verifier.setLogFileName("../log-build1.txt"); + verifier.setSystemProperty("maven.compiler.release", "17"); + verifier.executeGoal("verify"); + verifier.verifyErrorFreeLog(); + verifier.verifyTextInLog("Saved Build to local file"); + + // Second build with same release=17 should hit cache + verifier.setLogFileName("../log-build2.txt"); + verifier.setSystemProperty("maven.compiler.release", "17"); + verifier.executeGoals(Arrays.asList("clean", "verify")); + verifier.verifyErrorFreeLog(); + verifier.verifyTextInLog("Found cached build"); + + // Third build with different release=21 - reconciliation detects mismatch and triggers rebuild + verifier.setLogFileName("../log-build3.txt"); + verifier.setSystemProperty("maven.compiler.release", "21"); + verifier.executeGoals(Arrays.asList("clean", "verify")); + verifier.verifyErrorFreeLog(); + verifier.verifyTextInLog("Plugin parameter mismatch found"); + verifier.verifyTextInLog("Compiling"); + } + + @Test + void testDefaultReconciliationWithSourceTarget(Verifier verifier) throws VerificationException, IOException { + verifier.setAutoclean(false); + + // First build with source=11, target=11 + verifier.setLogFileName("../log-source1.txt"); + verifier.setSystemProperty("maven.compiler.source", "11"); + verifier.setSystemProperty("maven.compiler.target", "11"); + verifier.executeGoal("verify"); + verifier.verifyErrorFreeLog(); + verifier.verifyTextInLog("Saved Build to local file"); + + // Second build with different target=17 - reconciliation detects mismatch and triggers rebuild + verifier.setLogFileName("../log-source2.txt"); + verifier.setSystemProperty("maven.compiler.source", "11"); + verifier.setSystemProperty("maven.compiler.target", "17"); + verifier.executeGoals(Arrays.asList("clean", "verify")); + verifier.verifyErrorFreeLog(); + verifier.verifyTextInLog("Plugin parameter mismatch found"); + verifier.verifyTextInLog("Compiling"); + } +} + +/** + * Test that default reconciliation configs are still applied when executionControl exists + * but configures a different plugin. Ensures defaults and explicit configs are merged. + */ +@IntegrationTest("src/test/projects/default-reconciliation-with-other-plugin") +class DefaultReconciliationWithOtherPluginTest { + + @Test + void testDefaultReconciliationMergesWithExplicitConfig(Verifier verifier) + throws VerificationException, IOException { + verifier.setAutoclean(false); + + // First build with release=17 + verifier.setLogFileName("../log-merge1.txt"); + verifier.setSystemProperty("maven.compiler.release", "17"); + verifier.executeGoal("verify"); + verifier.verifyErrorFreeLog(); + verifier.verifyTextInLog("Saved Build to local file"); + + // Second build with release=21 - defaults still apply, reconciliation detects mismatch + // (defaults should still apply even though executionControl configures surefire) + verifier.setLogFileName("../log-merge2.txt"); + verifier.setSystemProperty("maven.compiler.release", "21"); + verifier.executeGoals(Arrays.asList("clean", "verify")); + verifier.verifyErrorFreeLog(); + verifier.verifyTextInLog("Plugin parameter mismatch found"); + verifier.verifyTextInLog("Compiling"); + } +} + +/** + * Test that explicit reconciliation config for a plugin OVERRIDES defaults, not merges. + * Explicit config should completely replace default config for that plugin. + */ +@IntegrationTest("src/test/projects/default-reconciliation-override") +class DefaultReconciliationOverrideTest { + + @Test + void testExplicitConfigOverridesDefaults(Verifier verifier) throws VerificationException, IOException { + verifier.setAutoclean(false); + + // First build with release=17 and source=11 + verifier.setLogFileName("../log-override1.txt"); + verifier.setSystemProperty("maven.compiler.release", "17"); + verifier.setSystemProperty("maven.compiler.source", "11"); + verifier.executeGoal("verify"); + verifier.verifyErrorFreeLog(); + verifier.verifyTextInLog("Saved Build to local file"); + + // Second build: Change source to 17 but keep release=17 + // Should HIT cache because explicit config only tracks 'release', not 'source' + // This proves explicit config OVERRIDES defaults (defaults would track source) + verifier.setLogFileName("../log-override2.txt"); + verifier.setSystemProperty("maven.compiler.release", "17"); + verifier.setSystemProperty("maven.compiler.source", "17"); + verifier.executeGoals(Arrays.asList("clean", "verify")); + verifier.verifyErrorFreeLog(); + verifier.verifyTextInLog("Found cached build"); + + // Third build: Change release to 21 - reconciliation detects mismatch and triggers rebuild + // (explicit tracking of 'release' catches the change) + verifier.setLogFileName("../log-override3.txt"); + verifier.setSystemProperty("maven.compiler.release", "21"); + verifier.setSystemProperty("maven.compiler.source", "17"); + verifier.executeGoals(Arrays.asList("clean", "verify")); + verifier.verifyErrorFreeLog(); + verifier.verifyTextInLog("Plugin parameter mismatch found"); + verifier.verifyTextInLog("Compiling"); + } +} diff --git a/src/test/java/org/apache/maven/buildcache/its/versioning/RemoteParentVersionBumpInvalidatesTest.java b/src/test/java/org/apache/maven/buildcache/its/versioning/RemoteParentVersionBumpInvalidatesTest.java index a6f9be5e..a832c354 100644 --- a/src/test/java/org/apache/maven/buildcache/its/versioning/RemoteParentVersionBumpInvalidatesTest.java +++ b/src/test/java/org/apache/maven/buildcache/its/versioning/RemoteParentVersionBumpInvalidatesTest.java @@ -75,6 +75,7 @@ void remoteParentVersionBumpInvalidatesCache() throws Exception { Verifier corpInstallV1 = new Verifier(corpParentDir); corpInstallV1.setAutoclean(false); corpInstallV1.setSystemProperty("projectVersion", System.getProperty("projectVersion")); + corpInstallV1.setSystemProperty("maven.build.cache.alwaysRunPlugins", "maven-install-plugin:install"); corpInstallV1.setLocalRepo(System.getProperty("localRepo")); corpInstallV1.setLogFileName("log-corp-v10-install.txt"); corpInstallV1.executeGoal("install"); @@ -100,6 +101,7 @@ void remoteParentVersionBumpInvalidatesCache() throws Exception { Verifier corpInstallV11 = new Verifier(corpParentDir); corpInstallV11.setAutoclean(false); corpInstallV11.setSystemProperty("projectVersion", System.getProperty("projectVersion")); + corpInstallV11.setSystemProperty("maven.build.cache.alwaysRunPlugins", "maven-install-plugin:install"); corpInstallV11.setLocalRepo(System.getProperty("localRepo")); corpInstallV11.setLogFileName("log-corp-v11-install.txt"); corpInstallV11.executeGoal("install"); diff --git a/src/test/java/org/apache/maven/buildcache/xml/AutoTrackingCacheKeyParametersTest.java b/src/test/java/org/apache/maven/buildcache/xml/AutoTrackingCacheKeyParametersTest.java new file mode 100644 index 00000000..6b905acb --- /dev/null +++ b/src/test/java/org/apache/maven/buildcache/xml/AutoTrackingCacheKeyParametersTest.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.buildcache.xml; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests that auto-tracking from plugin parameter definitions works correctly. + * Verifies that all cache-key parameters are automatically tracked, not just a subset. + */ +class AutoTrackingCacheKeyParametersTest { + + @Test + void testMavenCompilerPluginAutoTracksAllCacheKeyParameters() { + // This test verifies that the auto-generation tracks ALL cache-key parameters, + // not just the 3 that were in the old defaults.xml (source, target, release) + + PluginParameterLoader loader = new PluginParameterLoader(); + PluginParameterDefinition def = loader.load("maven-compiler-plugin"); + + assertNotNull(def, "Should load maven-compiler-plugin definition"); + + PluginParameterDefinition.GoalParameterDefinition compileGoal = def.getGoal("compile"); + assertNotNull(compileGoal, "compile goal should exist"); + + // Get all cache-key parameter names from the XML definition + Set cacheKeyParams = compileGoal.getParameters().values().stream() + .filter(PluginParameterDefinition.ParameterDefinition::isCacheKey) + .map(PluginParameterDefinition.ParameterDefinition::getName) + .collect(Collectors.toSet()); + + // Verify we have more than just the original 3 from defaults.xml + assertTrue( + cacheKeyParams.size() > 3, + "Should have more than 3 cache-key parameters (was: " + cacheKeyParams.size() + ")"); + + // Verify the original 3 are included + assertTrue(cacheKeyParams.contains("source"), "Should include 'source' parameter"); + assertTrue(cacheKeyParams.contains("target"), "Should include 'target' parameter"); + assertTrue(cacheKeyParams.contains("release"), "Should include 'release' parameter"); + + // Verify additional cache-key parameters are included (these were NOT in defaults.xml) + assertTrue( + cacheKeyParams.contains("encoding"), + "Should include 'encoding' parameter (auto-tracked, not in old defaults.xml)"); + assertTrue( + cacheKeyParams.contains("debug"), + "Should include 'debug' parameter (auto-tracked, not in old defaults.xml)"); + assertTrue( + cacheKeyParams.contains("compilerArgs"), + "Should include 'compilerArgs' parameter (auto-tracked, not in old defaults.xml)"); + assertTrue( + cacheKeyParams.contains("annotationProcessorPaths"), + "Should include 'annotationProcessorPaths' parameter (auto-tracked, not in old defaults.xml)"); + for (String paramName : new String[] { + "showWarnings", + "showDeprecation", + "fork", + "maxmem", + "meminitial", + "failOnError", + "failOnWarning", + "forceJavacCompilerUse", + "staleMillis", + "useIncrementalCompilation" + }) { + assertTrue( + cacheKeyParams.contains(paramName), + "Should include '" + paramName + "' because changing it can cause a cache miss"); + } + } + + @Test + void testMavenInstallPluginAutoTracksAllCacheKeyParameters() { + PluginParameterLoader loader = new PluginParameterLoader(); + PluginParameterDefinition def = loader.load("maven-install-plugin"); + + assertNotNull(def, "Should load maven-install-plugin definition"); + + PluginParameterDefinition.GoalParameterDefinition installGoal = def.getGoal("install"); + assertNotNull(installGoal, "install goal should exist"); + + // Get all cache-key parameter names + Set cacheKeyParams = installGoal.getParameters().values().stream() + .filter(PluginParameterDefinition.ParameterDefinition::isCacheKey) + .map(PluginParameterDefinition.ParameterDefinition::getName) + .collect(Collectors.toSet()); + + // The old defaults.xml had NO properties listed for maven-install-plugin + // Now auto-tracking should track all cache-key parameters + assertTrue(cacheKeyParams.size() > 0, "Should auto-track cache-key parameters (old defaults.xml had 0)"); + + // Coordinates are parameters of install-file; install exposes its output-affecting controls. + assertTrue(cacheKeyParams.contains("skip"), "Should track 'skip' parameter"); + assertTrue(cacheKeyParams.contains("installAtEnd"), "Should track 'installAtEnd' parameter"); + } + + @Test + void testNonCacheKeyParametersNotAutoTracked() { + PluginParameterLoader loader = new PluginParameterLoader(); + PluginParameterDefinition def = loader.load("maven-compiler-plugin"); + + assertNotNull(def); + + PluginParameterDefinition.GoalParameterDefinition compileGoal = def.getGoal("compile"); + assertNotNull(compileGoal); + + // Get all non-cache-key parameter names + Set nonCacheKeyParams = compileGoal.getParameters().values().stream() + .filter(param -> !param.isCacheKey()) + .map(PluginParameterDefinition.ParameterDefinition::getName) + .collect(Collectors.toSet()); + + // Verify non-cache-key parameters exist in the definition + assertTrue(nonCacheKeyParams.contains("verbose"), "Definition should include 'verbose' as non-cache-key"); + assertTrue( + nonCacheKeyParams.contains("compilerReuseStrategy"), + "Definition should include 'compilerReuseStrategy' as non-cache-key"); + + // Note: auto-generation only includes cache-key parameters, so these won't be tracked. + } +} diff --git a/src/test/java/org/apache/maven/buildcache/xml/PluginParameterValidationTest.java b/src/test/java/org/apache/maven/buildcache/xml/PluginParameterValidationTest.java new file mode 100644 index 00000000..62758e89 --- /dev/null +++ b/src/test/java/org/apache/maven/buildcache/xml/PluginParameterValidationTest.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.buildcache.xml; + +import org.apache.maven.buildcache.xml.PluginParameterDefinition.GoalParameterDefinition; +import org.apache.maven.buildcache.xml.PluginParameterDefinition.ParameterDefinition; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for plugin parameter definition loading and validation system + */ +class PluginParameterValidationTest { + + @Test + void testLoadMavenCompilerPlugin() { + PluginParameterLoader loader = new PluginParameterLoader(); + PluginParameterDefinition def = loader.load("maven-compiler-plugin"); + + assertNotNull(def, "Should load maven-compiler-plugin definition"); + assertEquals("org.apache.maven.plugins", def.getGroupId()); + assertEquals("maven-compiler-plugin", def.getArtifactId()); + + // Verify compile goal exists + GoalParameterDefinition compileGoal = def.getGoal("compile"); + assertNotNull(compileGoal, "compile goal should exist"); + + // Verify cache-key parameters + assertTrue(compileGoal.hasParameter("source"), "Should have 'source' parameter"); + assertTrue(compileGoal.hasParameter("target"), "Should have 'target' parameter"); + assertTrue(compileGoal.hasParameter("release"), "Should have 'release' parameter"); + + ParameterDefinition sourceParam = compileGoal.getParameter("source"); + assertTrue(sourceParam.isCacheKey()); + + // Parameters without cache-key metadata default to false + assertTrue(compileGoal.hasParameter("verbose"), "Should have 'verbose' parameter"); + ParameterDefinition verboseParam = compileGoal.getParameter("verbose"); + assertTrue(!verboseParam.isCacheKey()); + } + + @Test + void testLoadMavenInstallPlugin() { + PluginParameterLoader loader = new PluginParameterLoader(); + PluginParameterDefinition def = loader.load("maven-install-plugin"); + + assertNotNull(def, "Should load maven-install-plugin definition"); + assertEquals("org.apache.maven.plugins", def.getGroupId()); + assertEquals("maven-install-plugin", def.getArtifactId()); + + // Verify install goal exists + GoalParameterDefinition installGoal = def.getGoal("install"); + assertNotNull(installGoal, "install goal should exist"); + + // The lifecycle install goal exposes cache-key parameters. + assertTrue(installGoal.hasParameter("skip"), "Should have 'skip' parameter"); + ParameterDefinition skipParam = installGoal.getParameter("skip"); + assertTrue(skipParam.isCacheKey()); + } + + @Test + void testDefaultReconciliationParametersAreValid() { + PluginParameterLoader loader = new PluginParameterLoader(); + + // Verify that default reconciliation parameters for maven-compiler-plugin are valid + PluginParameterDefinition compilerDef = loader.load("maven-compiler-plugin"); + assertNotNull(compilerDef); + + GoalParameterDefinition compileGoal = compilerDef.getGoal("compile"); + assertNotNull(compileGoal); + + // All default parameters should exist and be cache keys + String[] defaultParams = {"source", "target", "release"}; + for (String paramName : defaultParams) { + assertTrue( + compileGoal.hasParameter(paramName), + "Default parameter '" + paramName + "' should exist in compile goal"); + + ParameterDefinition param = compileGoal.getParameter(paramName); + assertTrue(param.isCacheKey(), "Default parameter '" + paramName + "' should be a cache key"); + } + + // Verify testCompile goal has same parameters + GoalParameterDefinition testCompileGoal = compilerDef.getGoal("testCompile"); + assertNotNull(testCompileGoal); + + for (String paramName : defaultParams) { + assertTrue( + testCompileGoal.hasParameter(paramName), + "Default parameter '" + paramName + "' should exist in testCompile goal"); + } + } + + @Test + void testVersionSpecificParameterLoading() { + PluginParameterLoader loader = new PluginParameterLoader(); + + // Load for version 1.5.0 - should get 1.0.0 definition (highest minVersion <= 1.5.0) + PluginParameterDefinition def1 = loader.load("maven-versioned-plugin", "1.5.0"); + assertNotNull(def1, "Should load definition for version 1.5.0"); + assertEquals("1.0.0", def1.getMinVersion()); + + GoalParameterDefinition goal1 = def1.getGoal("execute"); + assertNotNull(goal1); + assertTrue(goal1.hasParameter("legacyParameter"), "Version 1.5.0 should have legacyParameter"); + assertTrue(goal1.hasParameter("commonParameter"), "Version 1.5.0 should have commonParameter"); + assertTrue(!goal1.hasParameter("newParameter"), "Version 1.5.0 should NOT have newParameter"); + + // Load for version 3.0.0 - should get 3.0.0 definition + PluginParameterDefinition def3 = loader.load("maven-versioned-plugin", "3.0.0"); + assertNotNull(def3, "Should load definition for version 3.0.0"); + assertEquals("3.0.0", def3.getMinVersion()); + + GoalParameterDefinition goal3 = def3.getGoal("execute"); + assertNotNull(goal3); + assertTrue(!goal3.hasParameter("legacyParameter"), "Version 3.0.0 should NOT have legacyParameter"); + assertTrue(goal3.hasParameter("commonParameter"), "Version 3.0.0 should have commonParameter"); + assertTrue(goal3.hasParameter("newParameter"), "Version 3.0.0 should have newParameter"); + + // Load for version 4.0.0 - should still get 3.0.0 definition (highest available) + PluginParameterDefinition def4 = loader.load("maven-versioned-plugin", "4.0.0"); + assertNotNull(def4, "Should load definition for version 4.0.0"); + assertEquals("3.0.0", def4.getMinVersion(), "Version 4.0.0 should use 3.0.0 definition"); + } + + @Test + void testVersionComparisonLogic() { + PluginParameterLoader loader = new PluginParameterLoader(); + + // Load for version with SNAPSHOT qualifier + PluginParameterDefinition defSnapshot = loader.load("maven-versioned-plugin", "1.5.0-SNAPSHOT"); + assertNotNull(defSnapshot, "Should handle SNAPSHOT versions"); + assertEquals("1.0.0", defSnapshot.getMinVersion()); + + // Load for version 2.9.9 - still in 1.x range + PluginParameterDefinition def2 = loader.load("maven-versioned-plugin", "2.9.9"); + assertNotNull(def2); + assertEquals("1.0.0", def2.getMinVersion(), "Version 2.9.9 should use 1.0.0 definition"); + + // Exact version match + PluginParameterDefinition defExact = loader.load("maven-versioned-plugin", "3.0.0"); + assertNotNull(defExact); + assertEquals("3.0.0", defExact.getMinVersion()); + } + + @Test + void testLoadWithoutVersion() { + PluginParameterLoader loader = new PluginParameterLoader(); + + // Load without version - should return first definition or one without minVersion + PluginParameterDefinition def = loader.load("maven-versioned-plugin", null); + assertNotNull(def, "Should load definition without version"); + + // For maven-compiler-plugin (which has no minVersion), should work fine + PluginParameterDefinition compilerDef = loader.load("maven-compiler-plugin", null); + assertNotNull(compilerDef); + } + + @Test + void testLoadPrefixedNamespaceDefinition() { + PluginParameterDefinition def = new PluginParameterLoader().load("maven-prefixed-plugin", "1.0.0"); + + assertNotNull(def); + assertNotNull(def.getGoal("execute").getParameter("source")); + } +} diff --git a/src/test/projects/default-reconciliation-override/.mvn/extensions.xml b/src/test/projects/default-reconciliation-override/.mvn/extensions.xml new file mode 100644 index 00000000..99dd711a --- /dev/null +++ b/src/test/projects/default-reconciliation-override/.mvn/extensions.xml @@ -0,0 +1,24 @@ + + + + + org.apache.maven.extensions + maven-build-cache-extension + ${projectVersion} + + diff --git a/src/test/projects/default-reconciliation-override/.mvn/maven-build-cache-config.xml b/src/test/projects/default-reconciliation-override/.mvn/maven-build-cache-config.xml new file mode 100644 index 00000000..59967216 --- /dev/null +++ b/src/test/projects/default-reconciliation-override/.mvn/maven-build-cache-config.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + diff --git a/src/test/projects/default-reconciliation-override/pom.xml b/src/test/projects/default-reconciliation-override/pom.xml new file mode 100644 index 00000000..42ec825a --- /dev/null +++ b/src/test/projects/default-reconciliation-override/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + + org.apache.maven.caching.test + default-reconciliation + 1.0-SNAPSHOT + + + 8 + 8 + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + diff --git a/src/test/projects/default-reconciliation-override/src/main/java/org/apache/maven/buildcache/Test.java b/src/test/projects/default-reconciliation-override/src/main/java/org/apache/maven/buildcache/Test.java new file mode 100644 index 00000000..e1e92cf2 --- /dev/null +++ b/src/test/projects/default-reconciliation-override/src/main/java/org/apache/maven/buildcache/Test.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.buildcache; + +public class Test { + public static void main(String[] args) { + System.out.println("Default reconciliation test"); + } +} diff --git a/src/test/projects/default-reconciliation-with-other-plugin/.mvn/extensions.xml b/src/test/projects/default-reconciliation-with-other-plugin/.mvn/extensions.xml new file mode 100644 index 00000000..99dd711a --- /dev/null +++ b/src/test/projects/default-reconciliation-with-other-plugin/.mvn/extensions.xml @@ -0,0 +1,24 @@ + + + + + org.apache.maven.extensions + maven-build-cache-extension + ${projectVersion} + + diff --git a/src/test/projects/default-reconciliation-with-other-plugin/.mvn/maven-build-cache-config.xml b/src/test/projects/default-reconciliation-with-other-plugin/.mvn/maven-build-cache-config.xml new file mode 100644 index 00000000..a18d3239 --- /dev/null +++ b/src/test/projects/default-reconciliation-with-other-plugin/.mvn/maven-build-cache-config.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + diff --git a/src/test/projects/default-reconciliation-with-other-plugin/pom.xml b/src/test/projects/default-reconciliation-with-other-plugin/pom.xml new file mode 100644 index 00000000..42ec825a --- /dev/null +++ b/src/test/projects/default-reconciliation-with-other-plugin/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + + org.apache.maven.caching.test + default-reconciliation + 1.0-SNAPSHOT + + + 8 + 8 + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + diff --git a/src/test/projects/default-reconciliation-with-other-plugin/src/main/java/org/apache/maven/buildcache/Test.java b/src/test/projects/default-reconciliation-with-other-plugin/src/main/java/org/apache/maven/buildcache/Test.java new file mode 100644 index 00000000..e1e92cf2 --- /dev/null +++ b/src/test/projects/default-reconciliation-with-other-plugin/src/main/java/org/apache/maven/buildcache/Test.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.buildcache; + +public class Test { + public static void main(String[] args) { + System.out.println("Default reconciliation test"); + } +} diff --git a/src/test/projects/default-reconciliation/.mvn/extensions.xml b/src/test/projects/default-reconciliation/.mvn/extensions.xml new file mode 100644 index 00000000..99dd711a --- /dev/null +++ b/src/test/projects/default-reconciliation/.mvn/extensions.xml @@ -0,0 +1,24 @@ + + + + + org.apache.maven.extensions + maven-build-cache-extension + ${projectVersion} + + diff --git a/src/test/projects/default-reconciliation/.mvn/maven-build-cache-config.xml b/src/test/projects/default-reconciliation/.mvn/maven-build-cache-config.xml new file mode 100644 index 00000000..287f130f --- /dev/null +++ b/src/test/projects/default-reconciliation/.mvn/maven-build-cache-config.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/src/test/projects/default-reconciliation/pom.xml b/src/test/projects/default-reconciliation/pom.xml new file mode 100644 index 00000000..42ec825a --- /dev/null +++ b/src/test/projects/default-reconciliation/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + + org.apache.maven.caching.test + default-reconciliation + 1.0-SNAPSHOT + + + 8 + 8 + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + diff --git a/src/test/projects/default-reconciliation/src/main/java/org/apache/maven/buildcache/Test.java b/src/test/projects/default-reconciliation/src/main/java/org/apache/maven/buildcache/Test.java new file mode 100644 index 00000000..e1e92cf2 --- /dev/null +++ b/src/test/projects/default-reconciliation/src/main/java/org/apache/maven/buildcache/Test.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.buildcache; + +public class Test { + public static void main(String[] args) { + System.out.println("Default reconciliation test"); + } +} diff --git a/src/test/resources/plugin-parameters/maven-prefixed-plugin.xml b/src/test/resources/plugin-parameters/maven-prefixed-plugin.xml new file mode 100644 index 00000000..57328cc2 --- /dev/null +++ b/src/test/resources/plugin-parameters/maven-prefixed-plugin.xml @@ -0,0 +1,36 @@ + + + + + org.apache.maven.plugins + maven-prefixed-plugin + 1.0.0 + + + execute + + + source + true + Source level; changing it can change generated test output + + + + + + diff --git a/src/test/resources/plugin-parameters/maven-test-plugin.xml b/src/test/resources/plugin-parameters/maven-test-plugin.xml new file mode 100644 index 00000000..de23d014 --- /dev/null +++ b/src/test/resources/plugin-parameters/maven-test-plugin.xml @@ -0,0 +1,41 @@ + + + + + org.apache.maven.plugins + maven-test-plugin + + + test + + + commonParameter + true + Parameter available in all versions; changing it changes the test result + + + oldParameter + true + Parameter only in 1.x versions; changing it changes the test result + + + + + diff --git a/src/test/resources/plugin-parameters/maven-versioned-plugin.xml b/src/test/resources/plugin-parameters/maven-versioned-plugin.xml new file mode 100644 index 00000000..8079854b --- /dev/null +++ b/src/test/resources/plugin-parameters/maven-versioned-plugin.xml @@ -0,0 +1,72 @@ + + + + + + + org.apache.maven.plugins + maven-versioned-plugin + 1.0.0 + + + execute + + + legacyParameter + true + Parameter available only in 1.x versions; changing it changes the test result + + + commonParameter + true + Parameter available in all versions; changing it changes the test result + + + + + + + + + org.apache.maven.plugins + maven-versioned-plugin + 3.0.0 + + + execute + + + newParameter + true + Parameter added in version 3.0.0; changing it changes the test result + + + commonParameter + true + Parameter available in all versions; changing it changes the test result + + + + + +