Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions DEV_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 2 additions & 0 deletions aQute.libg/test/aQute/lib/io/IOTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ public void testCreateSymlinkOrCopyWillDeleteOriginalLink(@InjectTemporaryDirect
}

@Test
@DisabledOnOs(WINDOWS)
public void testCreateDirectory_Symlink(@InjectTemporaryDirectory
Path rootDirectory) throws Exception {

Expand All @@ -357,6 +358,7 @@ public void testCreateDirectory_Symlink(@InjectTemporaryDirectory
}

@Test
@DisabledOnOs(WINDOWS)
public void testCreateDirectory_SymlinkMissingTarget(@InjectTemporaryDirectory
Path rootDirectory) throws Exception {

Expand Down
3 changes: 3 additions & 0 deletions biz.aQute.bndlib.tests/test/test/ProjectTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
5 changes: 3 additions & 2 deletions gradle-plugins/biz.aQute.bnd.gradle/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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 */
Expand All @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -600,7 +608,7 @@ private boolean isEmpty(String header) {
}
}

static final class AttributesMap extends AbstractMap<String, Object> {
static final class AttributesMap extends AbstractMap<String, Object> implements org.gradle.api.java.archives.Attributes {
final java.util.jar.Attributes source;

AttributesMap(java.util.jar.Attributes source) {
Expand Down Expand Up @@ -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 {

@bjhargrave bjhargrave Jul 28, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This choice can bite us one day. The gradle Manifest interface is really a ProviderType. Consumers like us are not meant to implement it. So we can be easily broken when gradle adds a new (non-default) method which could happen on a non-major version release.

So the trade-off seems to be implement their ProviderType interface and risk class load errors one day, or use the internal implementation class and risk that it is removed one day (probably on a major version release boundary).

That is why I originally implemented this using the internal implementation class since it seemed the least risky.

Another option would be to implement the interface using a dynamic proxy. See https://github.com/eclipse-osgi-technology/osgi-test/blob/main/org.osgi.test.common/src/main/java/org/osgi/test/common/context/CloseableBundleContext.java for an example of this. It is more robust if the ProviderType is extended in the future.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. I will have a look at the dyn proxy option.

private final org.gradle.api.java.archives.Attributes attributes;
private final Map<String, org.gradle.api.java.archives.Attributes> sections;

BuiltManifest(Manifest builtManifest) {
this.attributes = new AttributesMap(builtManifest.getMainAttributes());
Map<String, org.gradle.api.java.archives.Attributes> 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<String, org.gradle.api.java.archives.Attributes> getSections() {
return sections;
}

@Override
public org.gradle.api.java.archives.Manifest getEffectiveManifest() {
return this;
}

@Override
public org.gradle.api.java.archives.Manifest attributes(Map<String, ?> attributes) {
throw new UnsupportedOperationException("read-only manifest");
}

@Override
public org.gradle.api.java.archives.Manifest attributes(Map<String, ?> 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<org.gradle.api.java.archives.ManifestMergeSpec> action) {
throw new UnsupportedOperationException("read-only manifest");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Comment thread
bjhargrave marked this conversation as resolved.
.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")
Comment thread
bjhargrave marked this conversation as resolved.
.withProjectDir(testProjectDir)
.withArguments("-Pbnd_plugin=${pluginClasspath}", "--stacktrace", ":tasks")
.forwardOutput()
.build()

then: "the build succeeds"
result.task(":tasks").outcome == SUCCESS
}
}
4 changes: 2 additions & 2 deletions gradle-plugins/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this removed? It enables build scan.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put it back and updated it to the latest version of the deprecated/legacy plugin. Internet is full of suggestions to move to Develocity - but I agree on your point of staying with ths OSS version.

id("com.gradle.enterprise") version("3.19.2")
}
}

Expand Down
16 changes: 1 addition & 15 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import aQute.bnd.osgi.Constants

pluginManagement {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why removed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-introduced with updated version ... same as above

plugins {
id "com.gradle.enterprise" version "3.13.4"
id "com.gradle.enterprise" version "3.19.2"
}
}

Expand Down Expand Up @@ -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"
}
}
}

Comment on lines -53 to -66

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still deleted. Without this, we do not get a scan for each CI biuld.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I first commented, but haven't pushed everything. Please wait for a new "Review request".

rootProject.name = "bnd"

gradle.ext.bndWorkspaceConfigure = { workspace ->
Expand Down
Loading