Skip to content

Commit bd9ac00

Browse files
chrisruegerCopilot
andcommitted
Refactor rebuild policy into lifecycle plugin
Introduces a new `JarLifecycleListener` service hook in bndlib and wires `Project` to call listeners before/after JAR writes, replacing the in-core rebuild trigger policy logic. The old `Workspace` rebuild policy state and related `Constants` entries are removed, and the service package version is bumped to 4.11.0. Bndtools now owns rebuild-trigger behavior via `RebuildTriggerPolicy` and `RebuildTriggerPolicyPlugin`, which are registered from `Central.applyRebuildTriggerPolicy()` based on preferences. Preference/UI/explorer code is updated to use the new policy constants, and tests that depended on `Workspace#setRebuildTriggerPolicy` are temporarily commented with TODO markers. Add rebuild trigger policy plugin tests Move content-hash/API-digest rebuild behavior tests out of `ProjectTest` into a new `bndtools.core` test suite that exercises `RebuildTriggerPolicyPlugin` with dedicated workspace testdata. Add `ProjectTest` coverage for `JarLifecycleListener` integration, verifying `beforeWrite`/`afterWrite` invocation and shared context data flow. Also wire `biz.aQute.bnd.test` into `bndtools.core` testpath and remove stale commented cleanup lines. Signed-off-by: Christoph Rueger <chrisrueger@gmail.com> Document JAR lifecycle and rebuild policy plugins Add new plugin documentation for `JarLifecycleListener` and the Bndtools rebuild trigger policy, including lifecycle phases, context usage, registration, sidecar digests, and policy behavior (`always` vs `api`). Update the plugin overview with generic registration/error-handling guidance and retain repository tagging details in the index page. Signed-off-by: Christoph Rueger <chrisrueger@gmail.com> Co-Authored-By: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 4767395 commit bd9ac00

24 files changed

Lines changed: 1242 additions & 596 deletions

File tree

biz.aQute.bndlib.tests/test/test/ProjectTest.java

Lines changed: 50 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
import java.util.HashSet;
1919
import java.util.Iterator;
2020
import java.util.List;
21+
import java.util.Map;
2122
import java.util.Set;
23+
import java.util.concurrent.atomic.AtomicBoolean;
24+
import java.util.concurrent.atomic.AtomicReference;
2225
import java.util.jar.Manifest;
2326
import java.util.regex.Pattern;
2427

@@ -39,6 +42,7 @@
3942
import aQute.bnd.osgi.Processor;
4043
import aQute.bnd.osgi.Resource;
4144
import aQute.bnd.osgi.eclipse.EclipseClasspath;
45+
import aQute.bnd.service.JarLifecycleListener;
4246
import aQute.bnd.service.RepositoryPlugin;
4347
import aQute.bnd.service.Strategy;
4448
import aQute.bnd.test.jupiter.InjectTemporaryDirectory;
@@ -547,181 +551,71 @@ private void stale(Project project, boolean b) throws Exception {
547551
}
548552

549553
/**
550-
* Check that the content-hash optimization prevents unnecessary JAR
551-
* rewrites when the JAR content is unchanged between builds. This avoids
552-
* cascading rebuilds of dependent projects.
554+
* Verify that JarLifecycleListener plugins are called at the right times.
553555
*/
554556
@Test
555-
public void testContentHashSkipsBuildWhenUnchanged() throws Exception {
557+
public void testJarLifecycleListenerIntegration() throws Exception {
556558
Workspace ws = getWorkspace(IO.getFile("testresources/ws"));
557-
ws.setRebuildTriggerPolicy("api");
558559
Project project = ws.getProject("p-stale");
559560
assertNotNull(project);
560561

561-
// First build - should create JAR and digest files
562-
File[] firstBuild = project.build();
563-
assertNotNull(firstBuild);
564-
assertTrue(firstBuild.length > 0);
565-
566-
File jarFile = firstBuild[0];
567-
assertTrue(jarFile.isFile());
568-
long firstTimestamp = jarFile.lastModified();
569-
570-
// Verify digest file was created
571-
File digestFile = getDigestFile(jarFile);
572-
assertTrue(digestFile.isFile(), "Digest file should be created after build");
573-
String firstDigest = IO.collect(digestFile)
574-
.trim();
575-
assertFalse(firstDigest.isEmpty(), "Digest should not be empty");
576-
577-
// Simulate time passing by adjusting the JAR timestamp backward,
578-
// then mark the project as changed. If the optimization works
579-
// correctly, the JAR's timestamp will be restored to this value
580-
// since the content hasn't changed.
581-
long adjustedTimestamp = firstTimestamp - 10000;
582-
jarFile.setLastModified(adjustedTimestamp);
583-
584-
// Mark project as changed so it rebuilds
585-
project.setChanged();
586-
project.refresh();
587-
588-
// Second build - content is unchanged, JAR timestamp should be
589-
// preserved
590-
File[] secondBuild = project.build();
591-
assertNotNull(secondBuild);
592-
assertTrue(secondBuild.length > 0);
593-
594-
File jarFile2 = secondBuild[0];
595-
assertTrue(jarFile2.isFile());
596-
597-
// The JAR timestamp should be preserved since content didn't change
598-
assertEquals(adjustedTimestamp, jarFile2.lastModified(),
599-
"JAR timestamp should be preserved when content is unchanged");
600-
601-
// Digest file should still exist with the same content
602-
assertTrue(digestFile.isFile());
603-
String secondDigest = IO.collect(digestFile)
604-
.trim();
605-
assertEquals(firstDigest, secondDigest, "Digest should be unchanged");
606-
}
562+
// Register a mock listener to verify it's called
563+
AtomicBoolean beforeCalled = new AtomicBoolean();
564+
AtomicBoolean afterCalled = new AtomicBoolean();
607565

608-
/**
609-
* Check that when JAR content actually changes, the JAR is rewritten and
610-
* the digest is updated.
611-
*/
612-
@Test
613-
public void testContentHashRewritesWhenChanged() throws Exception {
614-
Workspace ws = getWorkspace(IO.getFile("testresources/ws"));
615-
ws.setRebuildTriggerPolicy("api");
616-
Project project = ws.getProject("p-stale");
617-
assertNotNull(project);
566+
JarLifecycleListener mockListener = new JarLifecycleListener() {
567+
@Override
568+
public void beforeWrite(Project p, Jar jar, File outputFile, Map<String, Object> context) {
569+
beforeCalled.set(true);
570+
}
618571

619-
// First build
620-
File[] firstBuild = project.build();
621-
assertNotNull(firstBuild);
622-
File jarFile = firstBuild[0];
623-
File digestFile = getDigestFile(jarFile);
624-
assertTrue(digestFile.isFile());
625-
String firstDigest = IO.collect(digestFile)
626-
.trim();
627-
628-
// Change the project content so the JAR will be different
629-
project.setChanged();
630-
project.refresh();
631-
project.setProperty("Include-Resource", "p;literal=\"changed content\"");
632-
633-
// Second build - content changed, JAR should be rewritten
634-
File[] secondBuild = project.build();
635-
assertNotNull(secondBuild);
636-
File jarFile2 = secondBuild[0];
637-
638-
// Digest should have changed
639-
assertTrue(digestFile.isFile());
640-
String secondDigest = IO.collect(digestFile)
641-
.trim();
642-
assertThat(secondDigest).as("Digest should change when content changes")
643-
.isNotEqualTo(firstDigest);
644-
}
572+
@Override
573+
public void afterWrite(Project p, File outputFile, Jar jar, Map<String, Object> context) {
574+
afterCalled.set(true);
575+
}
576+
};
645577

646-
private static File getDigestFile(File jarFile) {
647-
return new File(jarFile.getParentFile(), jarFile.getName() + ".digest");
578+
ws.addBasicPlugin(mockListener);
579+
580+
// Build
581+
File[] built = project.build();
582+
assertNotNull(built);
583+
assertThat(built.length).isGreaterThan(0);
584+
585+
// Verify listeners were called
586+
assertThat(beforeCalled).isTrue();
587+
assertThat(afterCalled).isTrue();
648588
}
649589

650590
/**
651-
* Check that the JAR timestamp is preserved when the content changes
652-
* Verify that the JAR's timestamp is preserved when the JAR content
653-
* changes but the exported API surface remains identical. The
654-
* optimization works by comparing a stored API-digest sidecar file
655-
* with the newly computed API digest during build. When they match,
656-
* the old file timestamp is kept so that downstream timestamp-based
657-
* staleness checks don't trigger unnecessary cascade rebuilds.
658-
* <p>
659-
* Since the p-stale test project is {@code -resourceonly} (no
660-
* exported packages), this test simulates the mechanism directly:
661-
* we write a known API digest, change the project content so the
662-
* full content digest changes, and verify that timestamp
663-
* preservation still engages via the API digest fallback.
591+
* Verify that the context map is passed correctly and allows data exchange.
664592
*/
665593
@Test
666-
public void testApiDigestPreservesTimestampWhenApiUnchanged() throws Exception {
594+
public void testJarArtifactWriteContextDataExchange() throws Exception {
667595
Workspace ws = getWorkspace(IO.getFile("testresources/ws"));
668-
ws.setRebuildTriggerPolicy("api");
669596
Project project = ws.getProject("p-stale");
670-
assertNotNull(project);
671597

672-
// First build — establishes the baseline JAR and digest files
673-
File[] firstBuild = project.build();
674-
assertNotNull(firstBuild);
675-
assertTrue(firstBuild.length > 0);
676-
677-
File jarFile = firstBuild[0];
678-
assertTrue(jarFile.isFile());
679-
680-
// Record the old timestamp and adjust it to simulate time
681-
long adjustedTimestamp = jarFile.lastModified() - 10000;
682-
jarFile.setLastModified(adjustedTimestamp);
683-
684-
// The content digest from the first build
685-
File digestFile = getDigestFile(jarFile);
686-
assertTrue(digestFile.isFile(), "Content digest file should exist after build");
687-
688-
// Write a fake API digest sidecar. The next build will produce
689-
// the same resource-only bundle (no Export-Package), so
690-
// calcApiDigest() returns null. However, when we change the
691-
// project content the content digest will differ. To simulate
692-
// the API-digest-unchanged path, we need a non-null API digest.
693-
// We'll change the content, then verify the mechanism by
694-
// checking the timestamp. Since calcApiDigest returns null for
695-
// resource-only bundles, the API digest path won't engage here,
696-
// but we can verify the infrastructure is correctly wired:
697-
// the API digest file should remain from the first build if no
698-
// new one is computed.
699-
File apiDigestFile = new File(jarFile.getParentFile(), jarFile.getName() + ".api-digest");
700-
701-
// Change the project content so the JAR will differ
702-
project.setChanged();
703-
project.refresh();
704-
project.setProperty("Include-Resource", "p;literal=\"changed content\"");
705-
706-
// Rebuild — content changed so content digest differs, and
707-
// calcApiDigest returns null for resource-only bundles. The
708-
// timestamp should NOT be preserved (no API digest fallback).
709-
File[] secondBuild = project.build();
710-
assertNotNull(secondBuild);
711-
712-
File jarFile2 = secondBuild[0];
713-
// Content changed so the digest should differ
714-
String newDigest = IO.collect(digestFile).trim();
715-
// The JAR should have a new timestamp since both content and
716-
// API digests indicate a change (or API digest is absent)
717-
assertThat(jarFile2.lastModified())
718-
.as("Timestamp should NOT be preserved when content changes and no API digest exists")
719-
.isNotEqualTo(adjustedTimestamp);
720-
721-
// The content digest file should still exist
722-
assertTrue(digestFile.isFile(), "Content digest file should exist after rebuild");
598+
AtomicReference<String> afterData = new AtomicReference<>();
599+
600+
JarLifecycleListener testListener = new JarLifecycleListener() {
601+
@Override
602+
public void beforeWrite(Project p, Jar jar, File outputFile, Map<String, Object> context) {
603+
context.put("testKey", "testValue");
604+
}
605+
606+
@Override
607+
public void afterWrite(Project p, File outputFile, Jar jar, Map<String, Object> context) {
608+
afterData.set((String) context.get("testKey"));
609+
}
610+
};
611+
612+
ws.addBasicPlugin(testListener);
613+
project.build();
614+
615+
assertThat(afterData.get()).isEqualTo("testValue");
723616
}
724617

618+
725619
/**
726620
* Check multiple repos
727621
*

0 commit comments

Comments
 (0)