diff --git a/DEV_README.md b/DEV_README.md index 5437146a72..671f699c16 100644 --- a/DEV_README.md +++ b/DEV_README.md @@ -645,3 +645,50 @@ Macros are in the following classes, recognized by methods starting with an unde - `biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java` - `biz.aQute.bndlib/src/aQute/bnd/osgi/Builder.java` - `biz.aQute.bndlib/src/aQute/bnd/build/Workspace.java` + +## Running builds with local Bnd Gradle Plugins + +If you are developing the Bnd Gradle Plugins in `gradle-plugins` and want a build to use your local plugin code, use one of the following approaches. + +### Rebuild the bnd workspace with freshly built local plugins + +For a full rebuild check (the same intent as CI), use the same multi-phase flow as the rebuild scripts: + +```bash +./.github/scripts/rebuild-build.sh +./.github/scripts/rebuild-test.sh +``` + +These scripts are used by the rebuild workflow in [.github/workflows/rebuild.yml](.github/workflows/rebuild.yml). +The manual equivalent commands are: + +```bash +./gradlew --no-daemon -Dmaven.repo.local=dist/m2 buildscriptDependencies publish +./gradlew --no-daemon -Dmaven.repo.local=dist/m2 --warning-mode=fail :gradle-plugins:build +./gradlew --no-daemon -Dmaven.repo.local=dist/m2 :gradle-plugins:publish +./gradlew --no-daemon -Dmaven.repo.local=dist/m2 -Pbnd_snapshots=./dist/bundles --warning-mode=fail buildscriptDependencies build publish +``` + +### Use the local plugin sources in another build (composite build) + +Use a Gradle composite build so the consumer build resolves the plugins directly from your local checkout. +In the consumer build's `settings.gradle`, add: + +```groovy +pluginManagement { + includeBuild("../bnd/gradle-plugins") +} +``` + +This approach avoids publishing and always uses the current local sources. + +### Publish locally and consume from `mavenLocal` + +Publish the plugin artifacts to your local Maven repository: + +```bash +./gradlew :gradle-plugins:biz.aQute.bnd.gradle:publishToMavenLocal +``` + +Then in the consumer build, use `mavenLocal()` in the `pluginManagement` repositories and pin the local version of the plugin. +If you update plugin code, re-run `publishToMavenLocal` before rebuilding the consumer. diff --git a/aQute.libg/test/aQute/lib/io/IOTest.java b/aQute.libg/test/aQute/lib/io/IOTest.java index c2a16dc6c8..06f0edf140 100644 --- a/aQute.libg/test/aQute/lib/io/IOTest.java +++ b/aQute.libg/test/aQute/lib/io/IOTest.java @@ -338,6 +338,7 @@ public void testCreateSymlinkOrCopyWillDeleteOriginalLink(@InjectTemporaryDirect } @Test + @DisabledOnOs(WINDOWS) public void testCreateDirectory_Symlink(@InjectTemporaryDirectory Path rootDirectory) throws Exception { @@ -357,6 +358,7 @@ public void testCreateDirectory_Symlink(@InjectTemporaryDirectory } @Test + @DisabledOnOs(WINDOWS) public void testCreateDirectory_SymlinkMissingTarget(@InjectTemporaryDirectory Path rootDirectory) throws Exception { diff --git a/biz.aQute.bndlib.tests/test/test/ProjectTest.java b/biz.aQute.bndlib.tests/test/test/ProjectTest.java index a595a73e4d..59f5a01fed 100644 --- a/biz.aQute.bndlib.tests/test/test/ProjectTest.java +++ b/biz.aQute.bndlib.tests/test/test/ProjectTest.java @@ -25,6 +25,8 @@ import org.assertj.core.api.SoftAssertions; import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.extension.ExtendWith; import aQute.bnd.build.Container; @@ -55,6 +57,7 @@ public class ProjectTest { File tmp; @Test + @DisabledOnOs(OS.WINDOWS) public void testAliasbuild() throws Exception { Workspace ws = getWorkspace(IO.getFile("testresources/ws")); Project project = ws.getProject("p3"); diff --git a/gradle-plugins/biz.aQute.bnd.gradle/build.gradle.kts b/gradle-plugins/biz.aQute.bnd.gradle/build.gradle.kts index 01f822c2d7..44f14315c8 100644 --- a/gradle-plugins/biz.aQute.bnd.gradle/build.gradle.kts +++ b/gradle-plugins/biz.aQute.bnd.gradle/build.gradle.kts @@ -1,5 +1,4 @@ import groovy.lang.GroovySystem -import org.gradle.util.internal.VersionNumber import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions import org.jetbrains.kotlin.gradle.dsl.KotlinVersion @@ -28,7 +27,9 @@ group = bnd_group version = bnd_version val groovyVersion = GroovySystem.getVersion() -val isGroovy4 = VersionNumber.parse(groovyVersion).major >= 4 +val groovyMajor = groovyVersion.substringBefore('.') + .toIntOrNull() ?: 0 +val isGroovy4 = groovyMajor >= 4 val spockVersion = if (isGroovy4) "2.3-groovy-4.0" else "2.3-groovy-3.0" val javaVersion = JavaVersion.VERSION_17 // Bnd target language level diff --git a/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/BndWorkspacePlugin.java b/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/BndWorkspacePlugin.java index cda339e1a6..f8cfbbe08d 100644 --- a/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/BndWorkspacePlugin.java +++ b/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/BndWorkspacePlugin.java @@ -10,6 +10,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.inject.Inject; + import aQute.bnd.build.Workspace; import aQute.bnd.exceptions.Exceptions; import aQute.bnd.osgi.Constants; @@ -21,6 +23,7 @@ import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.configuration.BuildFeatures; import org.gradle.api.file.Directory; import org.gradle.api.initialization.Settings; import org.gradle.api.internal.plugins.DslObject; @@ -30,6 +33,7 @@ import org.gradle.internal.metaobject.DynamicInvokeResult; import org.gradle.internal.metaobject.DynamicObject; import org.gradle.language.base.plugins.LifecycleBasePlugin; +import org.gradle.util.GradleVersion; /** * BndWorkspacePlugin for Gradle. @@ -228,6 +232,7 @@ private void configureSettings(Settings settings) throws Exception { } private void configureWorkspaceProject(Project workspace) throws Exception { + failFastOnConfigurationCache(workspace); Workspace bndWorkspace = getBndWorkspace(workspace); /* Configure the Bnd projects */ @@ -239,6 +244,45 @@ private void configureWorkspaceProject(Project workspace) throws Exception { } } + /** + * Fail fast when the configuration cache is requested since the Bnd + * Workspace plugin does not support the Gradle configuration cache. The Bnd + * Workspace model objects are not serializable by the configuration cache. + */ + private static void failFastOnConfigurationCache(Project workspace) { + if (isConfigurationCacheRequested(workspace)) { + throw new GradleException( + "The Bnd Workspace plugin does not support the Gradle configuration cache. " + + "Build with --no-configuration-cache and make sure the configuration cache is not enabled in a gradle.properties file (org.gradle.configuration-cache)."); + } + } + + private static boolean isConfigurationCacheRequested(Project workspace) { + if (GradleVersion.current() + .getBaseVersion() + .compareTo(GradleVersion.version("8.5")) < 0) { + /* The BuildFeatures service is not available before Gradle 8.5 */ + return false; + } + return workspace.getObjects() + .newInstance(BuildFeaturesQuery.class) + .getBuildFeatures() + .getConfigurationCache() + .getRequested() + .getOrElse(Boolean.FALSE); + } + + /** + * Holder type to obtain the {@link BuildFeatures} service via injection. + */ + static abstract class BuildFeaturesQuery { + @Inject + public BuildFeaturesQuery() {} + + @Inject + public abstract BuildFeatures getBuildFeatures(); + } + /** * Return the Bnd Workspace for the specified Gradle project. * 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 8e0270b8c3..48bbfcedf9 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 @@ -19,6 +19,7 @@ import java.util.AbstractSet; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -37,6 +38,7 @@ import aQute.lib.io.IO; import aQute.lib.strings.Strings; import aQute.lib.utf8properties.UTF8Properties; +import groovy.lang.Closure; import org.gradle.api.Action; import org.gradle.api.GradleException; import org.gradle.api.Project; @@ -46,7 +48,6 @@ import org.gradle.api.file.FileCollection; import org.gradle.api.file.ProjectLayout; import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.java.archives.internal.DefaultManifest; import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.MapProperty; @@ -582,11 +583,18 @@ public void execute(Task t) { } private org.gradle.api.java.archives.Manifest mergeManifest(Manifest builtManifest) { - org.gradle.api.java.archives.Manifest mergeManifest = new DefaultManifest(null); - mergeManifest.attributes(new AttributesMap(builtManifest.getMainAttributes())); - builtManifest.getEntries() - .forEach((section, attrs) -> mergeManifest.attributes(new AttributesMap(attrs), section)); - return mergeManifest; + /* + * Create a new Manifest object for use as the merge source, so we + * do not mutate the task's manifest. We use a read-only adapter + * implementing the public Manifest interface since Gradle offers no + * public factory for detached Manifest objects and the internal + * DefaultManifest type must not be used. Note that using + * taskManifest.getEffectiveManifest() to obtain a new object is not + * possible: when the manifest has no merge specs, it returns the + * task manifest object itself, so merging it back into the task + * manifest creates a merge cycle resulting in a StackOverflowError. + */ + return new BuiltManifest(builtManifest); } private void failTask(String msg, File archiveFile) { @@ -600,7 +608,7 @@ private boolean isEmpty(String header) { } } - static final class AttributesMap extends AbstractMap { + static final class AttributesMap extends AbstractMap implements org.gradle.api.java.archives.Attributes { final java.util.jar.Attributes source; AttributesMap(java.util.jar.Attributes source) { @@ -636,4 +644,69 @@ public int size() { }; } } + + /** + * A read-only {@link org.gradle.api.java.archives.Manifest} adapter over + * the manifest generated by the bnd builder. It is used as the merge + * source for the task's manifest, so only the query methods used by the + * Gradle manifest merge machinery are supported. + */ + static final class BuiltManifest implements org.gradle.api.java.archives.Manifest { + private final org.gradle.api.java.archives.Attributes attributes; + private final Map sections; + + BuiltManifest(Manifest builtManifest) { + this.attributes = new AttributesMap(builtManifest.getMainAttributes()); + Map sections = new LinkedHashMap<>(); + builtManifest.getEntries() + .forEach((section, attrs) -> sections.put(section, new AttributesMap(attrs))); + this.sections = Collections.unmodifiableMap(sections); + } + + @Override + public org.gradle.api.java.archives.Attributes getAttributes() { + return attributes; + } + + @Override + public Map getSections() { + return sections; + } + + @Override + public org.gradle.api.java.archives.Manifest getEffectiveManifest() { + return this; + } + + @Override + public org.gradle.api.java.archives.Manifest attributes(Map attributes) { + throw new UnsupportedOperationException("read-only manifest"); + } + + @Override + public org.gradle.api.java.archives.Manifest attributes(Map attributes, String sectionName) { + throw new UnsupportedOperationException("read-only manifest"); + } + + @Override + public org.gradle.api.java.archives.Manifest writeTo(Object path) { + throw new UnsupportedOperationException("read-only manifest"); + } + + @Override + public org.gradle.api.java.archives.Manifest from(Object... mergePath) { + throw new UnsupportedOperationException("read-only manifest"); + } + + @Override + public org.gradle.api.java.archives.Manifest from(Object mergePath, Closure closure) { + throw new UnsupportedOperationException("read-only manifest"); + } + + @Override + public org.gradle.api.java.archives.Manifest from(Object mergePath, + Action action) { + throw new UnsupportedOperationException("read-only manifest"); + } + } } diff --git a/gradle-plugins/biz.aQute.bnd.gradle/src/test/groovy/aQute/bnd/gradle/TestBndPlugin.groovy b/gradle-plugins/biz.aQute.bnd.gradle/src/test/groovy/aQute/bnd/gradle/TestBndPlugin.groovy index 99eb695a46..158e379d0d 100644 --- a/gradle-plugins/biz.aQute.bnd.gradle/src/test/groovy/aQute/bnd/gradle/TestBndPlugin.groovy +++ b/gradle-plugins/biz.aQute.bnd.gradle/src/test/groovy/aQute/bnd/gradle/TestBndPlugin.groovy @@ -514,4 +514,37 @@ class TestBndPlugin extends Specification { !generated.exists() !simple_bundle.exists() } + + def "Bnd Workspace Plugin configuration cache fail fast Test"() { + given: + String testProject = "workspaceplugin1" + File testProjectDir = new File(testResources, testProject) + assert testProjectDir.isDirectory() + + when: "the configuration cache is requested" + // "8.5" is a floor, not a pin: TestHelper.getGradleRunner(version) + // runs with max(version, default version for the current JVM). The + // BuildFeatures service used for the fail fast check was introduced + // in Gradle 8.5, so this test verifies the oldest supported Gradle + // version on JDK 17 while newer JVMs (and --warning-mode=fail runs) + // exercise Gradle 8.10/9.x with the same test. + def result = TestHelper.getGradleRunner("8.5") + .withProjectDir(testProjectDir) + .withArguments("-Pbnd_plugin=${pluginClasspath}", "--configuration-cache", "--stacktrace", ":tasks") + .forwardOutput() + .buildAndFail() + + then: "the build fails fast with a clear message" + result.output.contains("The Bnd Workspace plugin does not support the Gradle configuration cache") + + when: "the configuration cache is not requested" + result = TestHelper.getGradleRunner("8.5") + .withProjectDir(testProjectDir) + .withArguments("-Pbnd_plugin=${pluginClasspath}", "--stacktrace", ":tasks") + .forwardOutput() + .build() + + then: "the build succeeds" + result.task(":tasks").outcome == SUCCESS + } } diff --git a/gradle-plugins/settings.gradle.kts b/gradle-plugins/settings.gradle.kts index af9cb9fc72..b9c000d849 100644 --- a/gradle-plugins/settings.gradle.kts +++ b/gradle-plugins/settings.gradle.kts @@ -1,8 +1,8 @@ pluginManagement { plugins { - id("com.gradle.plugin-publish") version("1.2.0") + id("com.gradle.plugin-publish") version("2.1.1") id("dev.hargrave.addmavendescriptor") version("1.1.0") - id("com.gradle.enterprise") version("3.13.4") + id("com.gradle.enterprise") version("3.19.2") } } diff --git a/settings.gradle b/settings.gradle index 84883ef468..7012c591e9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ import aQute.bnd.osgi.Constants pluginManagement { plugins { - id "com.gradle.enterprise" version "3.13.4" + id "com.gradle.enterprise" version "3.19.2" } } @@ -50,20 +50,6 @@ buildscript { } } -plugins { - id "com.gradle.enterprise" -} - -if (Boolean.parseBoolean(System.getenv("CI"))) { - gradleEnterprise { - buildScan { - publishAlways() - termsOfServiceUrl = "https://gradle.com/terms-of-service" - termsOfServiceAgree = "yes" - } - } -} - rootProject.name = "bnd" gradle.ext.bndWorkspaceConfigure = { workspace ->