Skip to content

Commit

Permalink
bndtools#5346 Support for reading MRJs
Browse files Browse the repository at this point in the history
There has been a long history how to support MRJs (Multi Release Jars). I kind of get it since it seems a giant magnet for complications, but alas, progress.

The main problem with the current approach is that the support was focused on the Jar file. Imho, (and I think BJ's) this is the wrong place. The Jar abstracts working with the ZIP file but should imho not contain any semantics of the class path. We already placed the Bundle-Classpath (which is a very similar problem) in the Analyzer.

This morning I had an interesting discussion with Christopher :-)  He really helped me to see the problem more clear (I hope). I think we need to separate this problem strictly in 2 parts. The first is the MJR read support. This is this PR. Write will come later.

During the discussion it became clear to me that for our build (remember, I am ignoring writing MJRs) we need a _view_ on the MJR that flattens the namespace of the resources. To be able to flatten, we need a max release for the view. I am a bit fuzzy on the use cases but I am convinced you need to be able specify this max release per JAR if you need to but for convenience, we need a setting per builder/project and default default. I've come up with the following rules:

* In a -*path (e.g. -buildpath) you can set a MRJ.
* You can add an attribute like `-java.release.max=10` to the file in the buildpath. This will create a view up to and including release 10. I.e. what the VM would see if it was a Java 10 VM.
* In absence of this attribute, a default can be set in the workspace or project with the same property `-java.release.max=17`
* In absence of a default, the Jar is flattened for all the release it contains. This may mean that the compiler cannot read certain class files.

I am open to another strategy if someone has ideas. I am not sure I like the property name. We could use javac.target but I feel a tad uncomfortable with that. However, during the discussion it became crystal clear to me that this was not a Jar quality because the view that is used depends on the triplet of MRJ layout, max release, and the Jar.

So I've implemented this accordingly in this PR. Quick feedback is appreciated because Christopher has been hanging in there for a very long time.

---
 Signed-off-by: Peter Kriens <Peter.Kriens@aQute.biz>

Signed-off-by: Peter Kriens <Peter.Kriens@aQute.biz>
  • Loading branch information
pkriens committed Feb 21, 2023
1 parent 074e31e commit a953038
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 68 deletions.
Binary file not shown.
Binary file not shown.
105 changes: 76 additions & 29 deletions biz.aQute.bndlib.tests/test/test/AnalyzerTest.java
Expand Up @@ -7,6 +7,7 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
Expand All @@ -22,6 +23,8 @@

import org.junit.jupiter.api.Test;

import aQute.bnd.build.model.EE;
import aQute.bnd.classfile.ClassFile;
import aQute.bnd.header.Attrs;
import aQute.bnd.header.Parameters;
import aQute.bnd.osgi.About;
Expand All @@ -48,13 +51,67 @@ class T3 extends T2 {}
public class AnalyzerTest {
static File cwd = new File(System.getProperty("user.dir"));

/**
* Test the usage of a multi-release file
*/

@Test
public void testMultiRelease() throws Exception {
try (Builder source = new Builder()) {
source.addClasspath(new Jar(IO.getFile("jar/multi-released-lib-1.0-SNAPSHOT.jar")));
source.setProperty("-includepackage", "*");
Jar jar = source.build();
assertThat(source.check()).isTrue();

assertThat(jar.getResource("org/example/Hello11.class")).isNotNull();
assertThat(jar.getResource("org/example/Hello8.class")).isNotNull();
ClassFile helloClass = ClassFile
.parseClassFile(new DataInputStream(jar.getResource("org/example/Hello.class")
.openInputStream()));
assertThat(helloClass.major_version).isEqualTo(EE.JavaSE_11.getMajorVersion());
}

try (Builder source = new Builder()) {
source.addClasspath(new Jar(IO.getFile("jar/multi-released-lib-1.0-SNAPSHOT.jar")), 10);
source.setProperty("-includepackage", "*");
Jar jar = source.build();
assertThat(source.check()).isTrue();

assertThat(jar.getResource("org/example/Hello11.class")).isNull();
assertThat(jar.getResource("org/example/Hello8.class")).isNotNull();
ClassFile helloClass = ClassFile
.parseClassFile(new DataInputStream(jar.getResource("org/example/Hello.class")
.openInputStream()));
assertThat(helloClass.major_version).isEqualTo(EE.JavaSE_1_8.getMajorVersion());
}

}

/**
* Check a single file flattening
*/

@Test
public void testMultiReleaseFlattening() throws Exception {
Jar outer = new Jar(IO.getFile("jar/multi-release-gson-2.9.1.jar"));
assertThat(outer.getModuleName()).isNull();
try (Builder source = new Builder()) {
source.addClasspath(outer);
Jar inner = source.getClasspath()
.get(0);
assertThat(inner.getModuleName()).isEqualTo("com.google.gson");
assertThat(inner.getResource("com/google/gson/Gson.class")).isNotNull();
}

}

/**
* Verify that the manifest overrides a version in a package info
*/

@Test
public void testManifestOverridesPackageInfo() throws Exception {
try(Builder source = new Builder() ){
try (Builder source = new Builder()) {
source.setProperty("-exportcontents", "foo;version=1000");
source.setProperty("-includeresource", "foo/packageinfo;literal='version 1\n'");
Jar jar = source.build();
Expand Down Expand Up @@ -458,10 +515,8 @@ public void testExportContentsDirectory() throws Exception {
b.setProperty("-exportcontents", "test.refer");
b.build();
assertTrue(b.check("Bundle-ClassPath uses a directory 'jars/some.jar'"));
assertTrue(
b.getImports()
.getByFQN("org.osgi.service.event") != null,
b.getImports()
assertTrue(b.getImports()
.getByFQN("org.osgi.service.event") != null, b.getImports()
.toString());
} finally {
b.close();
Expand Down Expand Up @@ -1049,14 +1104,12 @@ public void testBundleActivatorNoType() throws Exception {
Manifest manifest = a.getJar()
.getManifest();

assertEquals(0,
a.getErrors()
.size(),
assertEquals(0, a.getErrors()
.size(),
a.getErrors()
.toString());
assertEquals(1,
a.getWarnings()
.size(),
assertEquals(1, a.getWarnings()
.size(),
a.getWarnings()
.toString());
assertTrue(a.check("A Bundle-Activator header was present but no activator class was defined"));
Expand All @@ -1083,14 +1136,12 @@ public void testBundleActivatorNotAType() throws Exception {
Manifest manifest = a.getJar()
.getManifest();

assertEquals(2,
a.getErrors()
.size(),
assertEquals(2, a.getErrors()
.size(),
a.getErrors()
.toString());
assertEquals(0,
a.getWarnings()
.size(),
assertEquals(0, a.getWarnings()
.size(),
a.getWarnings()
.toString());
assertTrue(a.check("A Bundle-Activator header is present and its value is not a valid type name 123",
Expand Down Expand Up @@ -1118,14 +1169,12 @@ public void testScanForABundleActivatorNoMatches() throws Exception {
Manifest manifest = a.getJar()
.getManifest();

assertEquals(1,
a.getErrors()
.size(),
assertEquals(1, a.getErrors()
.size(),
a.getErrors()
.toString());
assertEquals(0,
a.getWarnings()
.size(),
assertEquals(0, a.getWarnings()
.size(),
a.getWarnings()
.toString());
assertTrue(a.check(
Expand Down Expand Up @@ -1153,14 +1202,12 @@ public void testScanForABundleActivatorMultipleMatches() throws Exception {
Manifest manifest = a.getJar()
.getManifest();

assertEquals(1,
a.getErrors()
.size(),
assertEquals(1, a.getErrors()
.size(),
a.getErrors()
.toString());
assertEquals(0,
a.getWarnings()
.size(),
assertEquals(0, a.getWarnings()
.size(),
a.getWarnings()
.toString());
assertTrue(
Expand Down
6 changes: 3 additions & 3 deletions biz.aQute.bndlib/src/aQute/bnd/build/ProjectBuilder.java
Expand Up @@ -37,6 +37,7 @@
import aQute.bnd.osgi.BundleId;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Descriptors.TypeRef;
import aQute.bnd.osgi.Domain;
import aQute.bnd.osgi.EmbeddedResource;
import aQute.bnd.osgi.Instruction;
import aQute.bnd.osgi.Instructions;
Expand Down Expand Up @@ -229,7 +230,8 @@ private void addClasspath(Parameters dependencies, Container c) throws IOExcepti
return;
}
Jar jar = new Jar(file);
super.addClasspath(jar);
int releaseMax = getInt(Domain.domain(c.getAttributes()), file.getPath(), -1, Constants.JAVAC_RELEASE_MAX);
addClasspath(jar, releaseMax);
project.unreferencedClasspathEntries.put(jar.getName(), c);
Map<String, String> containerAttributes = c.getAttributes();
if ((dependencies != null)
Expand Down Expand Up @@ -842,7 +844,6 @@ public Jar[] builds() throws Exception {
return super.builds();
}


/**
* Called when we start to build a builder. We reset our map of bsn ->
* version and set the default contents of the bundle.
Expand All @@ -862,7 +863,6 @@ protected void startBuild(Builder builder) throws Exception {
}
}


/**
* Called when we're done with a builder. In this case we retrieve package
* information from builder.
Expand Down
79 changes: 45 additions & 34 deletions biz.aQute.bndlib/src/aQute/bnd/build/model/EE.java
Expand Up @@ -16,80 +16,83 @@

public enum EE {

OSGI_Minimum_1_0("OSGi/Minimum-1.0", "OSGi/Minimum", "1.0"),
OSGI_Minimum_1_0("OSGi/Minimum-1.0", "OSGi/Minimum", "1.0", 0),

OSGI_Minimum_1_1("OSGi/Minimum-1.1", "OSGi/Minimum", "1.1", OSGI_Minimum_1_0),
OSGI_Minimum_1_1("OSGi/Minimum-1.1", "OSGi/Minimum", "1.1", 1, OSGI_Minimum_1_0),

OSGI_Minimum_1_2("OSGi/Minimum-1.2", "OSGi/Minimum", "1.2", OSGI_Minimum_1_1),
OSGI_Minimum_1_2("OSGi/Minimum-1.2", "OSGi/Minimum", "1.2", 2, OSGI_Minimum_1_1),

JRE_1_1("JRE-1.1", "JRE", "1.1"),
JRE_1_1("JRE-1.1", "JRE", "1.1", 1),

J2SE_1_2("J2SE-1.2", "JavaSE", "1.2", JRE_1_1),
J2SE_1_2("J2SE-1.2", "JavaSE", "1.2", 2, JRE_1_1),

J2SE_1_3("J2SE-1.3", "JavaSE", "1.3", J2SE_1_2, OSGI_Minimum_1_1),
J2SE_1_3("J2SE-1.3", "JavaSE", "1.3", 3, J2SE_1_2, OSGI_Minimum_1_1),

J2SE_1_4("J2SE-1.4", "JavaSE", "1.4", J2SE_1_3, OSGI_Minimum_1_2),
J2SE_1_4("J2SE-1.4", "JavaSE", "1.4", 4, J2SE_1_3, OSGI_Minimum_1_2),

J2SE_1_5("J2SE-1.5", "JavaSE", "1.5", J2SE_1_4),
J2SE_1_5("J2SE-1.5", "JavaSE", "1.5", 5, J2SE_1_4),

JavaSE_1_6("JavaSE-1.6", "JavaSE", "1.6", J2SE_1_5),
JavaSE_1_6("JavaSE-1.6", "JavaSE", "1.6", 6, J2SE_1_5),

JavaSE_1_7("JavaSE-1.7", "JavaSE", "1.7", JavaSE_1_6),
JavaSE_1_7("JavaSE-1.7", "JavaSE", "1.7", 7, JavaSE_1_6),

JavaSE_compact1_1_8("JavaSE/compact1-1.8", "JavaSE/compact1", "1.8", OSGI_Minimum_1_2),
JavaSE_compact1_1_8("JavaSE/compact1-1.8", "JavaSE/compact1", "1.8", 8, OSGI_Minimum_1_2),

JavaSE_compact2_1_8("JavaSE/compact2-1.8", "JavaSE/compact2", "1.8", JavaSE_compact1_1_8),
JavaSE_compact2_1_8("JavaSE/compact2-1.8", "JavaSE/compact2", "1.8", 8, JavaSE_compact1_1_8),

JavaSE_compact3_1_8("JavaSE/compact3-1.8", "JavaSE/compact3", "1.8", JavaSE_compact2_1_8),
JavaSE_compact3_1_8("JavaSE/compact3-1.8", "JavaSE/compact3", "1.8", 8, JavaSE_compact2_1_8),

JavaSE_1_8("JavaSE-1.8", "JavaSE", "1.8", JavaSE_1_7, JavaSE_compact3_1_8),
JavaSE_1_8("JavaSE-1.8", "JavaSE", "1.8", 8, JavaSE_1_7, JavaSE_compact3_1_8),

JavaSE_9,
JavaSE_10,
JavaSE_11,
JavaSE_12,
JavaSE_13,
JavaSE_14,
JavaSE_15,
JavaSE_16,
JavaSE_17,
JavaSE_18,
JavaSE_19,
JavaSE_20,
JavaSE_21,
JavaSE_22,
JavaSE_23,
JavaSE_24,
JavaSE_9(9),
JavaSE_10(10),
JavaSE_11(11),
JavaSE_12(12),
JavaSE_13(13),
JavaSE_14(14),
JavaSE_15(15),
JavaSE_16(16),
JavaSE_17(17),
JavaSE_18(18),
JavaSE_19(19),
JavaSE_20(20),
JavaSE_21(21),
JavaSE_22(22),
JavaSE_23(23),
JavaSE_24(24),

UNKNOWN("<UNKNOWN>", "UNKNOWN", "0");
UNKNOWN("<UNKNOWN>", "UNKNOWN", "0", 0);

private final String eeName;
private final String capabilityName;
private final String versionLabel;
private final Version capabilityVersion;
private final EE[] compatible;
private final int release;
private transient EnumSet<EE> compatibleSet;
private transient Parameters packages = null;
private transient Parameters modules = null;

/**
* For use by JavaSE_9 and later.
*/
EE() {
EE(int release) {
int version = ordinal() - 5;
this.versionLabel = Integer.toString(version);
this.eeName = "JavaSE-" + versionLabel;
this.capabilityName = "JavaSE";
this.capabilityVersion = new Version(version);
this.compatible = null;
this.release = release;
}

EE(String eeName, String capabilityName, String versionLabel, EE... compatible) {
EE(String eeName, String capabilityName, String versionLabel, int release, EE... compatible) {
this.eeName = eeName;
this.capabilityName = capabilityName;
this.versionLabel = versionLabel;
this.capabilityVersion = new Version(versionLabel);
this.compatible = compatible;
this.release = release;
}

public String getEEName() {
Expand All @@ -106,6 +109,7 @@ public EE[] getCompatible() {
}

private static final EE[] values = values();

private Optional<EE> previous() {
int ordinal = ordinal() - 1;
if (ordinal >= 0) {
Expand Down Expand Up @@ -188,7 +192,7 @@ public static Optional<EE> highestFromTargetVersion(String targetVersion) {

public static EE parse(String str) {
for (EE ee : values) {
if (ee.eeName.equals(str))
if (ee.eeName.equalsIgnoreCase(str))
return ee;
}
return null;
Expand Down Expand Up @@ -240,4 +244,11 @@ private void init() {
throw Exceptions.duck(ioe);
}
}

public int getRelease() {
return release;
}
public int getMajorVersion() {
return release + 44;
}
}
@@ -1,4 +1,4 @@
@Version("4.2.0")
@Version("4.3.0")
package aQute.bnd.build.model;

import org.osgi.annotation.versioning.Version;
12 changes: 11 additions & 1 deletion biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java
Expand Up @@ -2528,10 +2528,20 @@ public List<Jar> getClasspath() {
warning("Cannot find entry on -classpath: %s", s);
}
}
return classpath;
return Collections.unmodifiableList(classpath);
}

public void addClasspath(Jar jar) {
addClasspath(jar, -1);
}

public void addClasspath(Jar jar, int release) {

if (release < 0) {
release = getInt(this, "addClasspath", 0, Constants.JAVAC_RELEASE_MAX);
}
jar = MultiReleaseJars.view(jar, release);

if (isPedantic() && jar.getResources()
.isEmpty())
warning("There is an empty jar or directory on the classpath: %s", jar.getName());
Expand Down

0 comments on commit a953038

Please sign in to comment.