001/* 002 * JDrupes Builder 003 * Copyright (C) 2026 Michael N. Lipp 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <https://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.builder.bnd; 020 021import aQute.bnd.osgi.Analyzer; 022import aQute.bnd.version.Version; 023import com.google.common.flogger.FluentLogger; 024import static com.google.common.flogger.LazyArgs.lazy; 025import io.vavr.control.Try; 026import java.io.File; 027import java.nio.file.Path; 028import java.util.Map; 029import java.util.Optional; 030import java.util.jar.Attributes; 031import java.util.jar.Manifest; 032import java.util.stream.Collectors; 033import java.util.stream.Stream; 034import org.jdrupes.builder.api.BuildException; 035import org.jdrupes.builder.api.Generator; 036import static org.jdrupes.builder.api.Intent.Consume; 037import static org.jdrupes.builder.api.Intent.Expose; 038import static org.jdrupes.builder.api.Intent.Reveal; 039import static org.jdrupes.builder.api.Intent.Supply; 040import org.jdrupes.builder.api.Project; 041import org.jdrupes.builder.api.Resource; 042import org.jdrupes.builder.api.ResourceProvider; 043import org.jdrupes.builder.api.ResourceRequest; 044import org.jdrupes.builder.api.ResourceType; 045import org.jdrupes.builder.api.Resources; 046import org.jdrupes.builder.api.RootProject; 047import org.jdrupes.builder.java.ClassTree; 048import org.jdrupes.builder.java.JavaCompiler; 049import static org.jdrupes.builder.java.JavaTypes.*; 050import org.jdrupes.builder.java.LibraryJarFile; 051import org.jdrupes.builder.java.ManifestAttributes; 052 053/// A [Generator] that computes OSGi metadata in response to requests for 054/// [ManifestAttributes]. 055/// 056/// This implementation uses the `bndlib` library from the 057/// [bnd](https://github.com/bndtools/bnd) project to analyze bundle 058/// contents and compute manifest attributes. 059/// 060/// When invoked, the analyzer first obtains resources of type [ClassTree] 061/// supplied to the project (typically by a [JavaCompiler]). These class 062/// trees are treated as the content of the bundle. 063/// 064/// It then obtains resources of type [LibraryJarFile] from the project's 065/// dependencies with intents `Consume`, `Reveal` and `Expose` (the same 066/// intents as used by the [JavaCompiler] when assembling the compilation 067/// classpath). These library resources are registered as bundle 068/// dependencies. 069/// 070/// The collected class tree and library resources are analyzed by `bndlib` 071/// to produce the manifest attributes requested. 072/// 073/// Contrary to most [ResourceProvider]s, the [BndAnalyzer] needs project 074/// specific informations (supplied as instructions). This can be handled 075/// in multiple ways. One approach is to add the [BndAnalyzer] with the 076/// instructions in the project’s constructor rather than in 077/// [RootProject#prepareProject]. Alternatively, put project-specific 078/// instructions in a `bnd.bnd` file in the project's directory, then 079/// register the analyzer in [RootProject#prepareProject] and add the 080/// instructions via [#instructions(Path)], where `Path` refers to the 081/// `bnd.bnd` file. 082/// 083@SuppressWarnings("PMD.TooManyStaticImports") 084public class BndAnalyzer extends AbstractBndGenerator { 085 086 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 087 088 /// Initializes a new osgi analyzer. 089 /// 090 /// @param project the project 091 /// 092 public BndAnalyzer(Project project) { 093 super(project); 094 } 095 096 /// Add the instruction specified by key and value. 097 /// 098 /// @param key the key 099 /// @param value the value 100 /// @return the bnd analyzer 101 /// 102 @Override 103 public BndAnalyzer instruction(String key, String value) { 104 super.instruction(key, value); 105 return this; 106 } 107 108 /// Add the given instructions for the analyzer. 109 /// 110 /// @param instructions the instructions 111 /// @return the bnd analyzer 112 /// 113 @Override 114 public BndAnalyzer instructions(Map<String, String> instructions) { 115 super.instructions(instructions); 116 return this; 117 } 118 119 /// Add the instructions from the given bnd (properties) file. 120 /// 121 /// @param bndFile the bnd file 122 /// @return the bnd analyzer 123 /// 124 @Override 125 public BndAnalyzer instructions(Path bndFile) { 126 super.instructions(bndFile); 127 return this; 128 } 129 130 @Override 131 @SuppressWarnings("PMD.AvoidCatchingGenericException") 132 protected <T extends Resource> Stream<T> 133 doProvide(ResourceRequest<T> requested) { 134 if (!requested.accepts(ManifestAttributesType)) { 135 return Stream.empty(); 136 } 137 try (var analyzer = new Analyzer(); 138 var jar = new aQute.bnd.osgi.Jar("dot")) { 139 // Assemble bundle content 140 var content = newResource(ClassTreesType).addAll(project() 141 .providers().resources(of(ClassTree.class).using(Supply))); 142 // A bnd ("better never document") Jar can actually be a 143 // classfile tree, and several such "Jar"s can be merged. 144 // IOException will be throw (.get()) and handled in the outer try 145 vavrStream(content).find(_ -> true).peek(t -> Try.of(() -> jar 146 .addAll(new aQute.bnd.osgi.Jar(t.root().toFile()))).get()); 147 analyzer.setJar(jar); 148 applyInstructions(analyzer); 149 150 // Add classpath dependencies 151 var bundleDeps = newResource( 152 new ResourceType<Resources<LibraryJarFile>>() {}).addAll( 153 project().providers(Consume, Reveal, Expose) 154 .resources(project().of(LibraryJarFileType))); 155 logger.atFiner().log("BndAnalyzer in" 156 + " %s uses dependencies %s", project(), 157 lazy(() -> bundleDeps.stream().map(e -> e.path().toString()) 158 .collect(Collectors.joining(File.pathSeparator)))); 159 // IOException will be throw (.get()) and handled in the outer try 160 vavrStream(bundleDeps).forEach(dep -> Try 161 .run(() -> analyzer.addClasspath(dep.path().toFile())).get()); 162 163 // Evaluate and convert to result type 164 var manifest = analyzer.calcManifest(); 165 verifyManifest(manifest); 166 var asResource = newResource(ManifestAttributesType); 167 asResource.putAll(manifest.getMainAttributes()); 168 @SuppressWarnings("unchecked") 169 var result = (T) asResource; 170 return Stream.of(result); 171 } catch (Exception e) { 172 throw new BuildException("Failed to generate OSGi metadata: %s", e) 173 .from(this).cause(e); 174 } 175 } 176 177 private void verifyManifest(Manifest manifest) { 178 Optional.ofNullable((String) manifest.getMainAttributes() 179 .get(new Attributes.Name("Bundle-Version"))).ifPresent(v -> { 180 try { 181 new Version(v); 182 } catch (IllegalArgumentException e) { 183 throw new BuildException("Attempt to specify invalid" 184 + " OSGi version %s", v).from(this).cause(e); 185 } 186 }); 187 188 } 189}