Skip to content

Commit

Permalink
Add multi-release processing support
Browse files Browse the repository at this point in the history
Currently bnd is not a capable of processing multi-release jars, this
adds support of multi-release processing by a new directive -release
where one can select what release version should be processed by bnd.

Fixes #5346

Signed-off-by: Christoph Läubrich <laeubi@laeubi-soft.de>
  • Loading branch information
laeubi committed Aug 23, 2022
1 parent b545b94 commit bfe1e83
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 20 deletions.
74 changes: 55 additions & 19 deletions biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java
Expand Up @@ -33,6 +33,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
Expand Down Expand Up @@ -200,12 +201,13 @@ public static Properties getManifest(File dirOrJar) throws Exception {
*/
public void analyze() throws Exception {
if (!analyzed) {
int release = getRelease();
analyzed = true;
analyzeContent();
analyzeContent(release);

// Execute any plugins
// TODO handle better reanalyze
doPlugins();
doPlugins(release);

//
// calculate class versions in use
Expand Down Expand Up @@ -423,6 +425,19 @@ public void analyze() throws Exception {
}
}

private int getRelease() {
String property = getProperty(JAVA_RELEASE);
if (property != null) {
try {
return Integer.parseInt(property.trim());
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
JAVA_RELEASE + " must be a valid integer but was " + property + " (" + e.getMessage() + ")", e);
}
}
return -1;
}

private void reset() {
contained.clear();
classspace.clear();
Expand All @@ -436,10 +451,10 @@ private void reset() {
bcpTypes.clear();
}

private void analyzeContent() throws Exception {
private void analyzeContent(int release) throws Exception {
// Parse all the classes in the
// the jar according to the OSGi Bundle-ClassPath
analyzeBundleClasspath();
analyzeBundleClasspath(release);

//
// Get exported packages from the
Expand Down Expand Up @@ -472,7 +487,7 @@ private void analyzeContent() throws Exception {

// Conditional packages

doConditionalPackages();
doConditionalPackages(release);
}

/**
Expand Down Expand Up @@ -625,7 +640,7 @@ public Clazz getPackageInfo(PackageRef packageRef) {
}
}

private void doConditionalPackages() throws Exception {
private void doConditionalPackages(int release) throws Exception {
//
// We need to find out the contained packages
// again ... so we need to clear any visited
Expand All @@ -636,7 +651,7 @@ private void doConditionalPackages() throws Exception {

for (Jar extra; (extra = getExtra()) != null;) {
dot.addAll(extra);
analyzeJar(extra, "", true, null);
analyzeJar(extra, "", true, null, release);
}
}

Expand Down Expand Up @@ -980,8 +995,10 @@ protected Jar getExtra() throws Exception {

/**
* Call AnalyzerPlugins to analyze the content.
*
* @param release the release flag for that content should be analyzed
*/
private void doPlugins() {
private void doPlugins(int release) {
List<AnalyzerPlugin> plugins = getPlugins(AnalyzerPlugin.class);
plugins.sort(Comparator.comparingInt(AnalyzerPlugin::ordering));
for (AnalyzerPlugin plugin : plugins) {
Expand All @@ -1000,7 +1017,7 @@ private void doPlugins() {
.filterValue(Objects::nonNull)
.collect(MapStream.toMap());
reset();
analyzeContent();
analyzeContent(release);
// Restore -internal-source information
// if the package still exists
sourceInformation.forEach((pkgRef, source) -> {
Expand Down Expand Up @@ -2577,11 +2594,11 @@ public Jar getTarget() {
return getJar();
}

private void analyzeBundleClasspath() throws Exception {
private void analyzeBundleClasspath(int release) throws Exception {
Parameters bcp = getBundleClasspath();

if (bcp.isEmpty()) {
analyzeJar(dot, "", true, null);
analyzeJar(dot, "", true, null, release);
} else {
// Cleanup entries
bcp = bcp.stream()
Expand All @@ -2593,7 +2610,7 @@ private void analyzeBundleClasspath() throws Exception {

for (String path : bcp.keySet()) {
if (path.equals(".")) {
analyzeJar(dot, "", okToIncludeDirs, null);
analyzeJar(dot, "", okToIncludeDirs, null, release);
continue;
}
//
Expand All @@ -2610,7 +2627,7 @@ private void analyzeBundleClasspath() throws Exception {
if (!(resource instanceof JarResource)) {
addClose(jar);
}
analyzeJar(jar, "", true, path);
analyzeJar(jar, "", true, path, release);
} catch (Exception e) {
warning("Invalid bundle classpath entry: %s: %s", path, e);
}
Expand All @@ -2623,7 +2640,7 @@ private void analyzeBundleClasspath() throws Exception {
warning(Constants.BUNDLE_CLASSPATH
+ " uses a directory '%s' as well as '.'. This means bnd does not know if a directory is a package.",
path);
analyzeJar(dot, path.concat("/"), true, path);
analyzeJar(dot, path.concat("/"), true, path, release);
} else {
Attrs info = bcp.get(path);
if (!"optional".equals(info.get(RESOLUTION_DIRECTIVE)))
Expand All @@ -2639,17 +2656,37 @@ private void analyzeBundleClasspath() throws Exception {
* contained and referred set and uses. This method ignores the Bundle
* classpath.
*/
private boolean analyzeJar(Jar jar, String prefix, boolean okToIncludeDirs, String bcpEntry) throws Exception {
private boolean analyzeJar(Jar jar, String prefix, boolean okToIncludeDirs, String bcpEntry, int release)
throws Exception {
Map<String, Clazz> mismatched = new HashMap<>();

Parameters importPackage = Optional.ofNullable(jar.getManifest())
.map(Domain::domain)
.map(Domain::getImportPackage)
.orElseGet(() -> new Parameters());

next: for (String path : jar.getResources()
.keySet()) {
if (path.startsWith(prefix)) {
Map<String, NavigableMap<Integer, Resource>> resources = jar.getVersionedResources();
next: for (Entry<String, NavigableMap<Integer, Resource>> entry : resources.entrySet()) {
String path = entry.getKey();
NavigableMap<Integer, Resource> releaseMap = entry.getValue();
Resource resource;
if (release < 9) {
// we need to get the default version
resource = releaseMap.get(Jar.MULTI_RELEASE_DEFAULT_VERSION);
} else {
// we need to get the highest resource for this release
Entry<Integer, Resource> versionEntry = releaseMap.headMap(release, true)
.lastEntry();
if (versionEntry == null) {
// there is a resource, but it is not available for this
// release, e.g. we analyze for release=11 and the resource
// is only for release=17
resource = null;
} else {
resource = versionEntry.getValue();
}
}
if (resource != null && path.startsWith(prefix)) {

String relativePath = path.substring(prefix.length());

Expand All @@ -2665,7 +2702,6 @@ private boolean analyzeJar(Jar jar, String prefix, boolean okToIncludeDirs, Stri

// Check class resources, we need to analyze them
if (path.endsWith(".class")) {
Resource resource = jar.getResource(path);
Clazz clazz;

try {
Expand Down
3 changes: 2 additions & 1 deletion biz.aQute.bndlib/src/aQute/bnd/osgi/Constants.java
Expand Up @@ -293,6 +293,7 @@ public interface Constants {
String WORKINGSET = "-workingset";
String WORKINGSET_MEMBER = "member";
String REQUIRE_BND = "-require-bnd";
String JAVA_RELEASE = "-release";

// Deprecated
String CLASSPATH = "-classpath";
Expand All @@ -314,7 +315,7 @@ public interface Constants {
CONNECTION_SETTINGS, RUNPROVIDEDCAPABILITIES, WORKINGSET, RUNSTORAGE, REPRODUCIBLE, INCLUDEPACKAGE,
CDIANNOTATIONS, REMOTEWORKSPACE, MAVEN_DEPENDENCIES, BUILDERIGNORE, STALECHECK, MAVEN_SCOPE, RUNSTARTLEVEL,
RUNOPTIONS, NOCLASSFORNAME, EXPORT_APIGUARDIAN, RESOLVE, DEFINE_CONTRACT, GENERATE, RUNFRAMEWORKRESTART,
NOIMPORTJAVA, VERSIONDEFAULTS, LIBRARY);
NOIMPORTJAVA, VERSIONDEFAULTS, LIBRARY, JAVA_RELEASE);

// Ignore bundle specific headers. These headers do not make a lot of sense
// to inherit
Expand Down
36 changes: 36 additions & 0 deletions biz.aQute.bndlib/src/aQute/bnd/osgi/Jar.java
Expand Up @@ -27,13 +27,16 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.Spliterator;
import java.util.Spliterators.AbstractSpliterator;
import java.util.TreeMap;
Expand Down Expand Up @@ -124,6 +127,11 @@ public enum Compression {
private long zipEntryConstantTime = ZIP_ENTRY_CONSTANT_TIME;
public static final Pattern METAINF_SIGNING_P = Pattern
.compile("META-INF/([^/]+\\.(?:DSA|RSA|EC|SF)|SIG-[^/]+)", Pattern.CASE_INSENSITIVE);
static final int MULTI_RELEASE_DEFAULT_VERSION = 0;
static final int MULTI_RELEASE_VERSION_GROUP = 1;
static final int MULTI_RELEASE_PATH_GROUP = 2;
static final Pattern MULTI_RELEASE_PATTERN = Pattern
.compile("^META-INF/versions/(\\d+)/(.*)$", Pattern.CASE_INSENSITIVE);

public Jar(String name) {
this.name = name;
Expand Down Expand Up @@ -432,6 +440,34 @@ public Map<String, Resource> getResources() {
return resources;
}

/**
* Computes a map of resources grouped by their versioned path according to
* a multi-release jar
*
* @return a map whose keys are plain resource names and the value is a map
* to all versioned variants, the map is modifiable but not backed
* by the resources of the jar, so changes in the the map will not
* be reflected in the jar or vice versa
*/
Map<String, NavigableMap<Integer, Resource>> getVersionedResources() {
Map<String, NavigableMap<Integer, Resource>> versionedResources = new HashMap<>();
for (Entry<String, Resource> entry : resources.entrySet()) {
Matcher matcher = Jar.MULTI_RELEASE_PATTERN.matcher(entry.getKey());
String path;
int version;
if (matcher.matches()) {
path = matcher.group(Jar.MULTI_RELEASE_PATH_GROUP);
version = Integer.parseInt(matcher.group(Jar.MULTI_RELEASE_VERSION_GROUP));
} else {
path = entry.getKey();
version = Jar.MULTI_RELEASE_DEFAULT_VERSION;
}
SortedMap<Integer, Resource> map = versionedResources.computeIfAbsent(path, nil -> new TreeMap<>());
map.put(version, entry.getValue());
}
return versionedResources;
}

public boolean addDirectory(Map<String, Resource> directory, boolean overwrite) {
check();
boolean duplicates = false;
Expand Down
13 changes: 13 additions & 0 deletions docs/_instructions/release.md
@@ -0,0 +1,13 @@
---
layout: default
class: Analyzer
title: -release NUMBER
summary: Specify the java release for which the Analyzer should generate meta-data.
---
The `-release` instruction is very similar to the `--release` option of javac and instructs the Analyser to process Multi-Release-JARs with the specified release as defined in [JEP 238: Multi-Release JAR Files](https://openjdk.org/jeps/238).

If the `-release` is not specified or NUMBER is smaller than 0 then release processing is **disabled** no further processing is done

If the `-release` is specified and NUMBER is smaller than or equal to 8 the **default content** is processed, that means for every jar entries in the META-INF/versions/* directories are effectively ignored by the processor.

If the `-release` is specified and NUMBER is larger or equal than 9 the content is processed as with the rules from JEP 238 possibly hiding some of the default content.

0 comments on commit bfe1e83

Please sign in to comment.