Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Generated manifest is now put into the output resources directory, to make sure that it's available at runtime for development.
- Fixed a bug in ProjectDepsPlugin where similarly-named jars might not replace all of the desired projects.
+ e.g. if you want to replace `durian-swt`, `durian-swt.os`, and `durian-swt.cocoa.macosx.x86_64`, in the old version `durian-swt` would not get replaced. Now fixed. ([#80](https://github.com/diffplug/goomph/pull/80))
- Add BndManifest support for every Jar task [(#79)](https://github.com/diffplug/goomph/pull/79)

### Version 3.16.0 - August 1st 2018 ([javadoc](http://diffplug.github.io/goomph/javadoc/3.16.0/), [jcenter](https://bintray.com/diffplug/opensource/goomph/3.16.0/view))

Expand Down
26 changes: 26 additions & 0 deletions src/main/java/com/diffplug/gradle/osgi/BndManifestExtension.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,19 @@
*/
package com.diffplug.gradle.osgi;

import java.util.*;

import org.gradle.api.plugins.JavaPlugin;
import org.gradle.util.GUtil;

/** Determines where the manifest is written out by {@link BndManifestPlugin}. */
public class BndManifestExtension {
public String copyFromTask = JavaPlugin.JAR_TASK_NAME;

public void copyFromTask(String copyFromTask) {
this.copyFromTask = copyFromTask;
}

public Object copyTo = null;

public void copyTo(Object copyTo) {
Expand All @@ -29,5 +40,20 @@ public void mergeWithExisting(boolean mergeWithExisting) {
this.mergeWithExisting = mergeWithExisting;
}

public Set<Object> includeTasks = new HashSet<>(Collections.singletonList(JavaPlugin.JAR_TASK_NAME));

public void includeTask(Object task) {
includeTasks.add(task);
}

public void includeTasks(Object... tasks) {
Collections.addAll(includeTasks, tasks);
}

public void setIncludeTasks(Iterable<?> includeTasks) {
this.includeTasks.clear();
GUtil.addToCollection(this.includeTasks, includeTasks);
}

static final String NAME = "osgiBndManifest";
}
108 changes: 62 additions & 46 deletions src/main/java/com/diffplug/gradle/osgi/BndManifestPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,11 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.function.Function;

import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.java.archives.Attributes;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginConvention;
Expand All @@ -39,22 +37,19 @@
import aQute.bnd.osgi.Builder;
import aQute.bnd.osgi.Constants;

import com.diffplug.common.base.Errors;
import com.diffplug.common.base.Predicates;
import com.diffplug.common.base.StringPrinter;
import com.diffplug.common.base.Throwing;
import com.diffplug.common.base.*;
import com.diffplug.common.collect.ImmutableMap;
import com.diffplug.gradle.FileMisc;
import com.diffplug.gradle.ProjectPlugin;
import com.diffplug.gradle.ZipMisc;

/**
* Generates a manifest using purely bnd, and outputs it for IDE consumption.
*
*
* Generating manifests by hand is a recipe for mistakes. Bnd does a fantastic
* job generating all this stuff for you, but there's a lot of wiring required
* to tie bnd into both Eclipse PDE and Gradle. Which is what Goomph is for!
*
*
* ```groovy
* apply plugin: 'com.diffplug.gradle.osgi.bndmanifest'
* // Pass headers and bnd directives: http://www.aqute.biz/Bnd/Format
Expand All @@ -76,17 +71,17 @@
* // By default, the existing manifest is completely ignored.
* // The line below will cause the existing manifest's fields
* // to be merged with the fields set by bnd.
* mergeWithExisting true
* mergeWithExisting true
* }
* ```
*
*
* Besides passing raw headers and bnd directives, this plugin also takes the following actions:
*
*
* * Passes the project version to bnd if {@code Bundle-Version} hasn't been set explicitly.
* * Replaces `-SNAPSHOT` in the version with `.IyyyyMMddkkmm` (to-the-minute timestamp).
* * Passes the {@code runtime} configuration's classpath to bnd for manifest calculation.
* * Instructs bnd to respect the result of the {@code processResources} task.
*
*
* Many thanks to JRuyi and Agemo Cui for their excellent
* [osgibnd-gradle-plugin](https://github.com/jruyi/osgibnd-gradle-plugin).
* This plugin follows the template set by their plugin, but with fewer automagic
Expand All @@ -97,36 +92,52 @@ public class BndManifestPlugin extends ProjectPlugin {
protected void applyOnce(Project proj) {
ProjectPlugin.getPlugin(proj, JavaPlugin.class);
BndManifestExtension extension = proj.getExtensions().create(BndManifestExtension.NAME, BndManifestExtension.class);
Jar jarTask = (Jar) proj.getTasks().getByName(JavaPlugin.JAR_TASK_NAME);
// at the end of the jar, modify the manifest, and possibly write it to `osgiBndManifest { bndManifestcopyTo }`.
jarTask.doLast(unused -> {
Errors.rethrow().run(() -> {
byte[] manifest = getManifestContent(jarTask, extension).getBytes(StandardCharsets.UTF_8);
// modify the jar
Map<String, Function<byte[], byte[]>> toModify = ImmutableMap.of("META-INF/MANIFEST.MF", in -> manifest);
ZipMisc.modify(jarTask.getArchivePath(), toModify, Predicates.alwaysFalse());
// write manifest to the output resources directory
Throwing.Consumer<Path> writeManifest = path -> {
if (Files.exists(path)) {
if (Arrays.equals(Files.readAllBytes(path), manifest)) {
return;

proj.afterEvaluate(project -> {

// copyFromTask must be configured if copyTo is used
Preconditions.checkArgument(extension.copyTo == null || extension.copyFromTask != null,
"copyFromTask can not be null if copyTo is set. Please provide a source task.");

final Jar copyFromTask = (extension.copyFromTask == null) ? null : getAsJar(proj, extension.copyFromTask);

Preconditions.checkArgument(extension.copyFromTask == null || extension.includeTasks.contains(extension.copyFromTask),
"copyFromTask must reside within includeTask");

extension.includeTasks.forEach(name -> {

Jar jarTask = getAsJar(proj, (String) name);

// at the end of the jar, modify the manifest
jarTask.doLast(unused -> {
Errors.rethrow().run(() -> {
byte[] manifest = getManifestContent(jarTask, extension).getBytes(StandardCharsets.UTF_8);
// modify the jar
Map<String, Function<byte[], byte[]>> toModify = ImmutableMap.of("META-INF/MANIFEST.MF", in -> manifest);
ZipMisc.modify(jarTask.getArchivePath(), toModify, Predicates.alwaysFalse());
// write manifest to the output resources directory
Throwing.Consumer<Path> writeManifest = path -> {
if (Files.exists(path)) {
if (Arrays.equals(Files.readAllBytes(path), manifest)) {
return;
}
}
Files.createDirectories(path.getParent());
Files.write(path, manifest);
};
writeManifest.accept(outputManifest(jarTask));

// and maybe write it to `osgiBndManifest { copyTo }`.
if (extension.copyTo != null && jarTask.equals(copyFromTask)) {
writeManifest.accept(jarTask.getProject().file(extension.copyTo).toPath());
}
}
Files.createDirectories(path.getParent());
Files.write(path, manifest);
};
writeManifest.accept(outputManifest(jarTask));
// and the jarTask, maybe
if (extension.copyTo != null) {
writeManifest.accept(jarTask.getProject().file(extension.copyTo).toPath());
}
});
});
});
});

proj.afterEvaluate(project -> {
// find the file that the user would like us to copy to (if any)
if (extension.copyTo != null) {
jarTask.getOutputs().file(extension.copyTo);
if (extension.copyTo != null && copyFromTask != null) {
copyFromTask.getOutputs().file(extension.copyTo);
}
});
}
Expand All @@ -142,7 +153,7 @@ private static String getManifestContent(Jar jarTask, BndManifestExtension exten
if (!extension.mergeWithExisting) {
Files.deleteIfExists(outputManifest(jarTask));
}
// take the bnd action
// take the bnd action
return BndManifestPlugin.takeBndAction(jarTask.getProject(), jarTask, jar -> {
return StringPrinter.buildString(printer -> {
try (OutputStream output = printer.toOutputStream(StandardCharsets.UTF_8)) {
Expand Down Expand Up @@ -173,15 +184,14 @@ private static String takeBndAction(Project project, Jar jarTask, Throwing.Funct
JavaPluginConvention javaConvention = project.getConvention().getPlugin(JavaPluginConvention.class);
SourceSetOutput main = javaConvention.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput();
// delete empty folders so that bnd doesn't make Export-Package entries for them
StringBuilder includeresource = new StringBuilder();
Set<String> includeresource = new LinkedHashSet<>();
deleteEmptyFoldersIfExists(main.getResourcesDir());
includeresource.append(fix(main.getResourcesDir()));
includeresource.add(fix(main.getResourcesDir()));
for (File file : main.getClassesDirs()) {
deleteEmptyFoldersIfExists(file);
includeresource.append(",");
includeresource.append(fix(file));
includeresource.add(fix(file));
}
builder.set(Constants.INCLUDERESOURCE, includeresource.toString());
builder.set(Constants.INCLUDERESOURCE, String.join(",", includeresource));

// set the version
if (builder.getBundleVersion() == null) {
Expand Down Expand Up @@ -215,4 +225,10 @@ private static String dateQualifier() {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddkkmm");
return dateFormat.format(new Date());
}

private static Jar getAsJar(Project project, String taskName) {
Task task = project.getTasks().getByName(taskName);
Preconditions.checkArgument(task instanceof Jar, "Task " + taskName + " must be a Jar derived task for generating BndManifest");
return (Jar) task;
}
}
105 changes: 104 additions & 1 deletion src/test/java/com/diffplug/gradle/osgi/BndManifestPluginTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,110 @@ public void assertMerging() throws IOException {
testCase("osgiBndManifest { mergeWithExisting true }", expectedMerge);
}

@Test
public void assertCustomJarTask() throws IOException {
write("src/main/resources/META-INF/MANIFEST.MF",
"Manifest-Version: 1.0",
"Bundle-ManifestVersion: 2",
"Bundle-Name: Mock",
"Bundle-SymbolicName: org.eclipse.e4.demo.e4photo.flickr.mock; singleton:=true",
"Bundle-Version: 1.0.0.qualifier",
"Bundle-ActivationPolicy: lazy",
"Require-Bundle: org.eclipse.core.runtime");
String expectedMerge = StringPrinter.buildStringFromLines(
"Manifest-Version: 1.0",
"Bundle-ActivationPolicy: lazy",
"Bundle-ManifestVersion: 2",
"Bundle-SymbolicName: test",
"Bundle-Version: 0.0.0.ERRORSETVERSION",
"Export-Package: test;uses:=\"com.diffplug.common.base\";version=\"0.0.0\"",
"Import-Package: com.diffplug.common.base;version=\"[3.4,4)\"",
"Require-Bundle: org.eclipse.core.runtime",
"Require-Capability: osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.8))\"");
String buildScript = StringPrinter.buildStringFromLines(
"task customJar(type: Jar) {",
" with jar",
" manifest.attributes(",
" '-exportcontents': 'test.*',",
" '-removeheaders': 'Bnd-LastModified,Bundle-Name,Created-By,Tool,Private-Package',",
" 'Bundle-SymbolicName': 'test'",
" )",
" classifier 'custom'",
"}",
"osgiBndManifest { ",
" mergeWithExisting true",
" includeTask 'customJar'",
"}");

testCase(buildScript, expectedMerge, "customJar");
}

@Test
public void assertCustomJarOnly() throws IOException {
write("src/main/resources/META-INF/MANIFEST.MF",
"Manifest-Version: 1.0",
"Bundle-ManifestVersion: 2",
"Bundle-Name: Mock",
"Bundle-SymbolicName: org.eclipse.e4.demo.e4photo.flickr.mock; singleton:=true",
"Bundle-Version: 1.0.0.qualifier",
"Bundle-ActivationPolicy: lazy",
"Require-Bundle: org.eclipse.core.runtime");

String expected = StringPrinter.buildStringFromLines(
"Manifest-Version: 1.0",
"Bundle-ActivationPolicy: lazy",
"Bundle-ManifestVersion: 2",
"Bundle-SymbolicName: test",
"Bundle-Version: 0.0.0.ERRORSETVERSION",
"Export-Package: test;uses:=\"com.diffplug.common.base\";version=\"0.0.0\"",
"Import-Package: com.diffplug.common.base;version=\"[3.4,4)\"",
"Require-Bundle: org.eclipse.core.runtime",
"Require-Capability: osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.8))\"");

String buildscriptAddendum = StringPrinter.buildStringFromLines(
"task customJar(type: Jar) {",
" with jar",
" manifest.attributes(",
" '-exportcontents': 'test.*',",
" '-removeheaders': 'Bnd-LastModified,Bundle-Name,Created-By,Tool,Private-Package',",
" 'Bundle-SymbolicName': 'test'",
" )",
" classifier 'custom'",
"}",
"osgiBndManifest { ",
" copyFromTask 'customJar'",
" mergeWithExisting true",
" includeTasks = ['customJar']",
"}");

testCase(buildscriptAddendum, expected, "customJar");
}

@Test
public void assertCustomJarExcluded() throws IOException {
String expected = StringPrinter.buildStringFromLines(
"Manifest-Version: 1.0",
"");

write("src/main/resources/META-INF/MANIFEST.MF",
"Manifest-Version: 1.0");

String buildscriptAddendum = StringPrinter.buildStringFromLines(
"task customJar(type: Jar) {",
" classifier 'custom'",
"}",
"osgiBndManifest { ",
" mergeWithExisting true",
"}");

testCase(buildscriptAddendum, expected, "customJar");
}

private void testCase(String buildscriptAddendum, String expectedManifest) throws IOException {
testCase(buildscriptAddendum, expectedManifest, "jar");
}

private void testCase(String buildscriptAddendum, String expectedManifest, String task) throws IOException {
write("build.gradle",
"plugins {",
" id 'java'",
Expand All @@ -98,7 +201,7 @@ private void testCase(String buildscriptAddendum, String expectedManifest) throw
" return new StringPrinter(str -> {});",
" }",
"}");
gradleRunner().withArguments("jar", "--stacktrace").build();
gradleRunner().withArguments(task, "--stacktrace").build();

// make sure the jar contains the proper manifest
File libsDir = file("build/libs");
Expand Down