From bfe1e83c00d066b94920a776f97975c58e0089ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Fri, 19 Aug 2022 11:02:00 +0200 Subject: [PATCH] Add multi-release processing support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 https://github.com/bndtools/bnd/issues/5346 Signed-off-by: Christoph Läubrich --- .../src/aQute/bnd/osgi/Analyzer.java | 74 ++++++++++++++----- .../src/aQute/bnd/osgi/Constants.java | 3 +- biz.aQute.bndlib/src/aQute/bnd/osgi/Jar.java | 36 +++++++++ docs/_instructions/release.md | 13 ++++ 4 files changed, 106 insertions(+), 20 deletions(-) create mode 100644 docs/_instructions/release.md diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java index 8fc29332ae7..4e526ceee1c 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java @@ -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; @@ -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 @@ -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(); @@ -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 @@ -472,7 +487,7 @@ private void analyzeContent() throws Exception { // Conditional packages - doConditionalPackages(); + doConditionalPackages(release); } /** @@ -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 @@ -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); } } @@ -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 plugins = getPlugins(AnalyzerPlugin.class); plugins.sort(Comparator.comparingInt(AnalyzerPlugin::ordering)); for (AnalyzerPlugin plugin : plugins) { @@ -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) -> { @@ -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() @@ -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; } // @@ -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); } @@ -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))) @@ -2639,7 +2656,8 @@ 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 mismatched = new HashMap<>(); Parameters importPackage = Optional.ofNullable(jar.getManifest()) @@ -2647,9 +2665,28 @@ private boolean analyzeJar(Jar jar, String prefix, boolean okToIncludeDirs, Stri .map(Domain::getImportPackage) .orElseGet(() -> new Parameters()); - next: for (String path : jar.getResources() - .keySet()) { - if (path.startsWith(prefix)) { + Map> resources = jar.getVersionedResources(); + next: for (Entry> entry : resources.entrySet()) { + String path = entry.getKey(); + NavigableMap 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 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()); @@ -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 { diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Constants.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Constants.java index 66ea322bfc4..5fa1c3adcb3 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Constants.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Constants.java @@ -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"; @@ -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 diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Jar.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Jar.java index 10ec3fd0f88..e8a3fa4ae4e 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Jar.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Jar.java @@ -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; @@ -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; @@ -432,6 +440,34 @@ public Map 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> getVersionedResources() { + Map> versionedResources = new HashMap<>(); + for (Entry 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 map = versionedResources.computeIfAbsent(path, nil -> new TreeMap<>()); + map.put(version, entry.getValue()); + } + return versionedResources; + } + public boolean addDirectory(Map directory, boolean overwrite) { check(); boolean duplicates = false; diff --git a/docs/_instructions/release.md b/docs/_instructions/release.md new file mode 100644 index 00000000000..4e9e8702b5e --- /dev/null +++ b/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. \ No newline at end of file