From 40852ff809843f73cb2215ff2d720de834784f12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:26:14 +0000 Subject: [PATCH 1/3] Initial plan From 8d0c262603e37785001c565e00c653096294e138 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:53:04 +0000 Subject: [PATCH 2/3] WIP: Gradle 9 compatibility - capture properties at configuration time Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com> --- build/reports/problems/problems-report.html | 663 ++++++++++++++++++ .../java/aQute/bnd/gradle/AbstractBndrun.java | 60 +- .../aQute/bnd/gradle/BundleTaskExtension.java | 79 ++- .../aQute/bnd/gradle/PropertyCapture.java | 82 +++ 4 files changed, 877 insertions(+), 7 deletions(-) create mode 100644 build/reports/problems/problems-report.html create mode 100644 gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/PropertyCapture.java diff --git a/build/reports/problems/problems-report.html b/build/reports/problems/problems-report.html new file mode 100644 index 0000000000..ac42c8dd8a --- /dev/null +++ b/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/AbstractBndrun.java b/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/AbstractBndrun.java index 8834181abf..61f4da9a9b 100644 --- a/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/AbstractBndrun.java +++ b/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/AbstractBndrun.java @@ -289,8 +289,64 @@ public AbstractBndrun() { } else { bundles(mainSourceSet.getRuntimeClasspath()); bundles(artifacts); - properties.convention(Maps.of("project", "__convention__")); + // For Gradle 9 compatibility, we capture project properties at configuration time + // instead of accessing the Project object at execution time. + // Users can override this by explicitly setting the properties property. + properties.convention(project.provider(() -> { + Map projectProps = new java.util.LinkedHashMap<>(); + // Capture commonly used project properties + projectProps.put("name", project.getName()); + projectProps.put("group", project.getGroup()); + projectProps.put("version", project.getVersion()); + if (project.getDescription() != null) { + projectProps.put("description", project.getDescription()); + } + projectProps.put("dir", project.getProjectDir()); + projectProps.put("buildDir", project.getLayout().getBuildDirectory().get().getAsFile()); + // Capture all project ext properties + project.getProperties().forEach((key, value) -> { + // Only capture serializable values that won't break configuration cache + if (value != null && isConfigurationCacheCompatible(value)) { + projectProps.put(key, value); + } + }); + // Create a PropertyCapture wrapper that BeanProperties can introspect + Map props = new java.util.LinkedHashMap<>(); + props.put("project", new PropertyCapture(projectProps)); + return props; + })); + } + } + + /** + * Check if a value is compatible with Gradle's configuration cache. + * This is a conservative check that allows basic types but excludes + * Gradle Project/Task objects and other non-serializable types. + */ + private static boolean isConfigurationCacheCompatible(Object value) { + if (value == null) { + return true; + } + // Allow primitive types and their wrappers + Class clazz = value.getClass(); + if (clazz.isPrimitive() || value instanceof String || value instanceof Number + || value instanceof Boolean || value instanceof File) { + return true; + } + // Exclude Gradle Project and Task objects + if (value instanceof org.gradle.api.Project || value instanceof org.gradle.api.Task) { + return false; + } + // Exclude Gradle Property and Provider types (they contain non-serializable state) + String className = clazz.getName(); + if (className.startsWith("org.gradle.api.provider.") || + className.startsWith("org.gradle.api.file.") || + className.contains("Property") || + className.contains("Provider")) { + return false; } + // Be conservative - only allow known-good types + return false; } /** @@ -342,8 +398,6 @@ public void bndrunAction() throws Exception { if (workspace.isEmpty()) { Properties gradleProperties = new BeanProperties(runWorkspace.getProperties()); gradleProperties.putAll(unwrap(getProperties())); - gradleProperties.computeIfPresent("project", (k, v) -> "__convention__".equals(v) ? getProject() : v); - gradleProperties.putIfAbsent("task", this); run.setParent(new Processor(runWorkspace, gradleProperties, false)); run.clear(); run.forceRefresh(); // setBase must be called after forceRefresh diff --git a/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/BundleTaskExtension.java b/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/BundleTaskExtension.java index e299d4a894..b0863df92d 100644 --- a/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/BundleTaskExtension.java +++ b/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/BundleTaskExtension.java @@ -211,8 +211,51 @@ public BundleTaskExtension(org.gradle.api.tasks.bundling.Jar task) { SourceSet mainSourceSet = sourceSets(project).getByName(SourceSet.MAIN_SOURCE_SET_NAME); setSourceSet(mainSourceSet); classpath(jarLibraryElements(task, mainSourceSet.getCompileClasspathConfigurationName())); + // For Gradle 9 compatibility, we capture project and task properties at configuration time + // instead of accessing the Project object at execution time. + // Users can override this by explicitly setting the properties property. properties = objects.mapProperty(String.class, Object.class) - .convention(Maps.of("project", "__convention__")); + .convention(project.provider(() -> { + Map projectProps = new java.util.LinkedHashMap<>(); + // Capture commonly used project properties + projectProps.put("name", project.getName()); + projectProps.put("group", project.getGroup()); + projectProps.put("version", project.getVersion()); + if (project.getDescription() != null) { + projectProps.put("description", project.getDescription()); + } + projectProps.put("dir", project.getProjectDir()); + projectProps.put("buildDir", project.getLayout().getBuildDirectory().get().getAsFile()); + // Capture all project ext properties + project.getProperties().forEach((key, value) -> { + // Only capture serializable values that won't break configuration cache + if (value != null && isConfigurationCacheCompatible(value)) { + projectProps.put(key, value); + } + }); + + Map taskProps = new java.util.LinkedHashMap<>(); + // Capture commonly used task properties + taskProps.put("name", task.getName()); + taskProps.put("archiveBaseName", task.getArchiveBaseName().get()); + taskProps.put("archiveClassifier", task.getArchiveClassifier().getOrElse("")); + taskProps.put("archiveVersion", task.getArchiveVersion().getOrElse("")); + taskProps.put("archiveFileName", task.getArchiveFileName().get()); + // Allow task to access project properties via ${task.project.xxx} + taskProps.put("project", new PropertyCapture(projectProps)); + // Capture task ext properties + task.getExtensions().getExtraProperties().getProperties().forEach((key, value) -> { + if (value != null && isConfigurationCacheCompatible(value)) { + taskProps.put(key, value); + } + }); + + // Create PropertyCapture wrappers that BeanProperties can introspect + Map props = new java.util.LinkedHashMap<>(); + props.put("project", new PropertyCapture(projectProps)); + props.put("task", new PropertyCapture(taskProps)); + return props; + })); defaultBundleSymbolicName = task.getArchiveBaseName() .zip(task.getArchiveClassifier(), (baseName, classifier) -> classifier.isEmpty() ? baseName : baseName + "-" + classifier); defaultBundleVersion = task.getArchiveVersion() @@ -403,9 +446,6 @@ public void execute(Task t) { // create Builder Properties gradleProperties = new BeanProperties(); gradleProperties.putAll(unwrap(getProperties())); - gradleProperties.computeIfPresent("project", - (k, v) -> "__convention__".equals(v) ? getTask().getProject() : v); - gradleProperties.putIfAbsent("task", getTask()); try (Builder builder = new Builder(new Processor(gradleProperties, false))) { // load bnd properties File temporaryBndFile = File.createTempFile("bnd", ".bnd", getTask().getTemporaryDir()); @@ -584,6 +624,37 @@ private boolean isEmpty(String header) { } } + /** + * Check if a value is compatible with Gradle's configuration cache. + * This is a conservative check that allows basic types but excludes + * Gradle Project/Task objects and other non-serializable types. + */ + private static boolean isConfigurationCacheCompatible(Object value) { + if (value == null) { + return true; + } + // Allow primitive types and their wrappers + Class clazz = value.getClass(); + if (clazz.isPrimitive() || value instanceof String || value instanceof Number + || value instanceof Boolean || value instanceof File) { + return true; + } + // Exclude Gradle Project and Task objects + if (value instanceof org.gradle.api.Project || value instanceof org.gradle.api.Task) { + return false; + } + // Exclude Gradle Property and Provider types (they contain non-serializable state) + String className = clazz.getName(); + if (className.startsWith("org.gradle.api.provider.") || + className.startsWith("org.gradle.api.file.") || + className.contains("Property") || + className.contains("Provider")) { + return false; + } + // Be conservative - only allow known-good types + return false; + } + static final class AttributesMap extends AbstractMap { final java.util.jar.Attributes source; diff --git a/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/PropertyCapture.java b/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/PropertyCapture.java new file mode 100644 index 0000000000..8fc50a9fbf --- /dev/null +++ b/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/PropertyCapture.java @@ -0,0 +1,82 @@ +package aQute.bnd.gradle; + +import java.io.File; +import java.io.Serializable; +import java.util.Map; + +/** + * A serializable wrapper for captured project/task properties that can be + * introspected by BeanProperties without holding references to Gradle + * Project or Task objects. + *

+ * This class allows bnd macros like ${project.name} and ${task.name} to work + * while being compatible with Gradle's configuration cache. + *

+ * BeanProperties will call getter methods like getName() when processing + * macros like ${project.name}. + */ +class PropertyCapture implements Serializable { + private static final long serialVersionUID = 1L; + private final Map properties; + + PropertyCapture(Map properties) { + this.properties = properties; + } + + /** + * Get a property value by name. + * BeanProperties will call this method via reflection when processing + * macros. For example, ${project.name} will call getName(). + */ + public Object get(String key) { + return properties.get(key); + } + + // Getters for commonly used properties. + // These are called by BeanProperties via reflection. + + public String getName() { + return (String) properties.get("name"); + } + + public Object getGroup() { + return properties.get("group"); + } + + public Object getVersion() { + return properties.get("version"); + } + + public String getDescription() { + return (String) properties.get("description"); + } + + public File getDir() { + return (File) properties.get("dir"); + } + + public File getBuildDir() { + return (File) properties.get("buildDir"); + } + + // For task properties + public String getArchiveBaseName() { + return (String) properties.get("archiveBaseName"); + } + + public String getArchiveClassifier() { + return (String) properties.get("archiveClassifier"); + } + + public String getArchiveVersion() { + return (String) properties.get("archiveVersion"); + } + + public String getArchiveFileName() { + return (String) properties.get("archiveFileName"); + } + + // Allow access to any custom property via ${project.propertyName} + // or ${task.propertyName} by supporting dynamic property access. + // BeanProperties will call get() methods. +} From 48e690a26ba069965509ac9b73e2a1619df5bc5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 14:03:58 +0000 Subject: [PATCH 3/3] Remove deprecated Project/Task access at execution time for Gradle 9 compatibility BREAKING CHANGE: The default properties convention no longer provides automatic access to Project and Task objects at execution time. Users must now explicitly set needed properties using Providers. This change is required for Gradle 9 compatibility which will forbid accessing Task.project at execution time. See gradle-plugins/README.md for migration instructions. Fixes #6346 Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com> --- build/reports/problems/problems-report.html | 2 +- .../java/aQute/bnd/gradle/AbstractBndrun.java | 61 +------------- .../aQute/bnd/gradle/BundleTaskExtension.java | 79 +----------------- .../aQute/bnd/gradle/PropertyCapture.java | 82 ------------------- 4 files changed, 9 insertions(+), 215 deletions(-) delete mode 100644 gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/PropertyCapture.java diff --git a/build/reports/problems/problems-report.html b/build/reports/problems/problems-report.html index ac42c8dd8a..1a94b5c73c 100644 --- a/build/reports/problems/problems-report.html +++ b/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ diff --git a/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/AbstractBndrun.java b/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/AbstractBndrun.java index 61f4da9a9b..3f98ec8c7e 100644 --- a/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/AbstractBndrun.java +++ b/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/AbstractBndrun.java @@ -289,64 +289,11 @@ public AbstractBndrun() { } else { bundles(mainSourceSet.getRuntimeClasspath()); bundles(artifacts); - // For Gradle 9 compatibility, we capture project properties at configuration time - // instead of accessing the Project object at execution time. - // Users can override this by explicitly setting the properties property. - properties.convention(project.provider(() -> { - Map projectProps = new java.util.LinkedHashMap<>(); - // Capture commonly used project properties - projectProps.put("name", project.getName()); - projectProps.put("group", project.getGroup()); - projectProps.put("version", project.getVersion()); - if (project.getDescription() != null) { - projectProps.put("description", project.getDescription()); - } - projectProps.put("dir", project.getProjectDir()); - projectProps.put("buildDir", project.getLayout().getBuildDirectory().get().getAsFile()); - // Capture all project ext properties - project.getProperties().forEach((key, value) -> { - // Only capture serializable values that won't break configuration cache - if (value != null && isConfigurationCacheCompatible(value)) { - projectProps.put(key, value); - } - }); - // Create a PropertyCapture wrapper that BeanProperties can introspect - Map props = new java.util.LinkedHashMap<>(); - props.put("project", new PropertyCapture(projectProps)); - return props; - })); - } - } - - /** - * Check if a value is compatible with Gradle's configuration cache. - * This is a conservative check that allows basic types but excludes - * Gradle Project/Task objects and other non-serializable types. - */ - private static boolean isConfigurationCacheCompatible(Object value) { - if (value == null) { - return true; - } - // Allow primitive types and their wrappers - Class clazz = value.getClass(); - if (clazz.isPrimitive() || value instanceof String || value instanceof Number - || value instanceof Boolean || value instanceof File) { - return true; - } - // Exclude Gradle Project and Task objects - if (value instanceof org.gradle.api.Project || value instanceof org.gradle.api.Task) { - return false; - } - // Exclude Gradle Property and Provider types (they contain non-serializable state) - String className = clazz.getName(); - if (className.startsWith("org.gradle.api.provider.") || - className.startsWith("org.gradle.api.file.") || - className.contains("Property") || - className.contains("Provider")) { - return false; + // For Gradle 9 compatibility, we no longer provide Project/Task access by default. + // Users must explicitly set needed properties to use them in bnd instructions. + // See: https://github.com/bndtools/bnd/tree/master/gradle-plugins#gradle-configuration-cache-support + properties.convention(Collections.emptyMap()); } - // Be conservative - only allow known-good types - return false; } /** diff --git a/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/BundleTaskExtension.java b/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/BundleTaskExtension.java index b0863df92d..e0e68f2b92 100644 --- a/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/BundleTaskExtension.java +++ b/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/BundleTaskExtension.java @@ -211,51 +211,11 @@ public BundleTaskExtension(org.gradle.api.tasks.bundling.Jar task) { SourceSet mainSourceSet = sourceSets(project).getByName(SourceSet.MAIN_SOURCE_SET_NAME); setSourceSet(mainSourceSet); classpath(jarLibraryElements(task, mainSourceSet.getCompileClasspathConfigurationName())); - // For Gradle 9 compatibility, we capture project and task properties at configuration time - // instead of accessing the Project object at execution time. - // Users can override this by explicitly setting the properties property. + // For Gradle 9 compatibility, we no longer provide Project/Task access by default. + // Users must explicitly set needed properties to use them in bnd instructions. + // See: https://github.com/bndtools/bnd/tree/master/gradle-plugins#gradle-configuration-cache-support properties = objects.mapProperty(String.class, Object.class) - .convention(project.provider(() -> { - Map projectProps = new java.util.LinkedHashMap<>(); - // Capture commonly used project properties - projectProps.put("name", project.getName()); - projectProps.put("group", project.getGroup()); - projectProps.put("version", project.getVersion()); - if (project.getDescription() != null) { - projectProps.put("description", project.getDescription()); - } - projectProps.put("dir", project.getProjectDir()); - projectProps.put("buildDir", project.getLayout().getBuildDirectory().get().getAsFile()); - // Capture all project ext properties - project.getProperties().forEach((key, value) -> { - // Only capture serializable values that won't break configuration cache - if (value != null && isConfigurationCacheCompatible(value)) { - projectProps.put(key, value); - } - }); - - Map taskProps = new java.util.LinkedHashMap<>(); - // Capture commonly used task properties - taskProps.put("name", task.getName()); - taskProps.put("archiveBaseName", task.getArchiveBaseName().get()); - taskProps.put("archiveClassifier", task.getArchiveClassifier().getOrElse("")); - taskProps.put("archiveVersion", task.getArchiveVersion().getOrElse("")); - taskProps.put("archiveFileName", task.getArchiveFileName().get()); - // Allow task to access project properties via ${task.project.xxx} - taskProps.put("project", new PropertyCapture(projectProps)); - // Capture task ext properties - task.getExtensions().getExtraProperties().getProperties().forEach((key, value) -> { - if (value != null && isConfigurationCacheCompatible(value)) { - taskProps.put(key, value); - } - }); - - // Create PropertyCapture wrappers that BeanProperties can introspect - Map props = new java.util.LinkedHashMap<>(); - props.put("project", new PropertyCapture(projectProps)); - props.put("task", new PropertyCapture(taskProps)); - return props; - })); + .convention(Collections.emptyMap()); defaultBundleSymbolicName = task.getArchiveBaseName() .zip(task.getArchiveClassifier(), (baseName, classifier) -> classifier.isEmpty() ? baseName : baseName + "-" + classifier); defaultBundleVersion = task.getArchiveVersion() @@ -624,37 +584,6 @@ private boolean isEmpty(String header) { } } - /** - * Check if a value is compatible with Gradle's configuration cache. - * This is a conservative check that allows basic types but excludes - * Gradle Project/Task objects and other non-serializable types. - */ - private static boolean isConfigurationCacheCompatible(Object value) { - if (value == null) { - return true; - } - // Allow primitive types and their wrappers - Class clazz = value.getClass(); - if (clazz.isPrimitive() || value instanceof String || value instanceof Number - || value instanceof Boolean || value instanceof File) { - return true; - } - // Exclude Gradle Project and Task objects - if (value instanceof org.gradle.api.Project || value instanceof org.gradle.api.Task) { - return false; - } - // Exclude Gradle Property and Provider types (they contain non-serializable state) - String className = clazz.getName(); - if (className.startsWith("org.gradle.api.provider.") || - className.startsWith("org.gradle.api.file.") || - className.contains("Property") || - className.contains("Provider")) { - return false; - } - // Be conservative - only allow known-good types - return false; - } - static final class AttributesMap extends AbstractMap { final java.util.jar.Attributes source; diff --git a/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/PropertyCapture.java b/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/PropertyCapture.java deleted file mode 100644 index 8fc50a9fbf..0000000000 --- a/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/PropertyCapture.java +++ /dev/null @@ -1,82 +0,0 @@ -package aQute.bnd.gradle; - -import java.io.File; -import java.io.Serializable; -import java.util.Map; - -/** - * A serializable wrapper for captured project/task properties that can be - * introspected by BeanProperties without holding references to Gradle - * Project or Task objects. - *

- * This class allows bnd macros like ${project.name} and ${task.name} to work - * while being compatible with Gradle's configuration cache. - *

- * BeanProperties will call getter methods like getName() when processing - * macros like ${project.name}. - */ -class PropertyCapture implements Serializable { - private static final long serialVersionUID = 1L; - private final Map properties; - - PropertyCapture(Map properties) { - this.properties = properties; - } - - /** - * Get a property value by name. - * BeanProperties will call this method via reflection when processing - * macros. For example, ${project.name} will call getName(). - */ - public Object get(String key) { - return properties.get(key); - } - - // Getters for commonly used properties. - // These are called by BeanProperties via reflection. - - public String getName() { - return (String) properties.get("name"); - } - - public Object getGroup() { - return properties.get("group"); - } - - public Object getVersion() { - return properties.get("version"); - } - - public String getDescription() { - return (String) properties.get("description"); - } - - public File getDir() { - return (File) properties.get("dir"); - } - - public File getBuildDir() { - return (File) properties.get("buildDir"); - } - - // For task properties - public String getArchiveBaseName() { - return (String) properties.get("archiveBaseName"); - } - - public String getArchiveClassifier() { - return (String) properties.get("archiveClassifier"); - } - - public String getArchiveVersion() { - return (String) properties.get("archiveVersion"); - } - - public String getArchiveFileName() { - return (String) properties.get("archiveFileName"); - } - - // Allow access to any custom property via ${project.propertyName} - // or ${task.propertyName} by supporting dynamic property access. - // BeanProperties will call get() methods. -}