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.ext.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.Collection; 029import java.util.Collections; 030import java.util.List; 031import java.util.Map; 032import java.util.Optional; 033import java.util.jar.Attributes; 034import java.util.jar.Manifest; 035import java.util.stream.Collectors; 036import org.jdrupes.builder.api.BuildException; 037import org.jdrupes.builder.api.ConfigurationException; 038import org.jdrupes.builder.api.Generator; 039import static org.jdrupes.builder.api.Intent.Consume; 040import static org.jdrupes.builder.api.Intent.Expose; 041import static org.jdrupes.builder.api.Intent.Reveal; 042import static org.jdrupes.builder.api.Intent.Supply; 043import org.jdrupes.builder.api.Project; 044import org.jdrupes.builder.api.Resource; 045import org.jdrupes.builder.api.ResourceProvider; 046import org.jdrupes.builder.api.ResourceRequest; 047import org.jdrupes.builder.api.ResourceType; 048import org.jdrupes.builder.api.Resources; 049import org.jdrupes.builder.api.RootProject; 050import org.jdrupes.builder.java.ClassTree; 051import org.jdrupes.builder.java.JavaCompiler; 052import static org.jdrupes.builder.java.JavaTypes.*; 053import org.jdrupes.builder.java.LibraryJarFile; 054import org.jdrupes.builder.java.ManifestAttributes; 055 056/// A [Generator] that computes OSGi metadata in response to requests for 057/// [ManifestAttributes]. 058/// 059/// This implementation uses the `bndlib` library from the 060/// [bnd](https://github.com/bndtools/bnd) project to analyze bundle 061/// contents and compute manifest attributes. 062/// 063/// When invoked, the analyzer first obtains resources of type [ClassTree] 064/// supplied to the project (typically by a [JavaCompiler]). These class 065/// trees are treated as the content of the bundle. 066/// 067/// It then obtains resources of type [LibraryJarFile] from the project's 068/// dependencies with intents `Consume`, `Reveal` and `Expose` (the same 069/// intents as used by the [JavaCompiler] when assembling the compilation 070/// classpath). These library resources are registered as bundle 071/// dependencies. 072/// 073/// The collected class tree and library resources are analyzed by `bndlib` 074/// to produce the manifest attributes requested. 075/// 076/// Contrary to most [ResourceProvider]s, the [BndAnalyzer] needs project 077/// specific informations (supplied as instructions). This can be handled 078/// in multiple ways. One approach is to add the [BndAnalyzer] with the 079/// instructions in the project’s constructor rather than in 080/// [RootProject#prepareProject]. Alternatively, put project-specific 081/// instructions in a `bnd.bnd` file in the project's directory, then 082/// register the analyzer in [RootProject#prepareProject] and add the 083/// instructions via [#instructions(Path)], where `Path` refers to the 084/// `bnd.bnd` file. 085/// 086/// This provider is made available as an extension. 087/// [ 089/// ](https://mvnrepository.com/artifact/org.jdrupes/jdbld-ext-bnd) 090/// 091public class BndAnalyzer extends AbstractBndGenerator { 092 093 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 094 095 /// Initializes a new osgi analyzer. 096 /// 097 /// @param project the project 098 /// 099 public BndAnalyzer(Project project) { 100 super(project); 101 } 102 103 /// Add the instruction specified by key and value. 104 /// 105 /// @param key the key 106 /// @param value the value 107 /// @return the bnd analyzer 108 /// 109 @Override 110 public BndAnalyzer instruction(String key, String value) { 111 super.instruction(key, value); 112 return this; 113 } 114 115 /// Add the given instructions for the analyzer. 116 /// 117 /// @param instructions the instructions 118 /// @return the bnd analyzer 119 /// 120 @Override 121 public BndAnalyzer instructions(Map<String, String> instructions) { 122 super.instructions(instructions); 123 return this; 124 } 125 126 /// Add the instructions from the given bnd (properties) file. 127 /// 128 /// @param bndFile the bnd file 129 /// @return the bnd analyzer 130 /// 131 @Override 132 public BndAnalyzer instructions(Path bndFile) { 133 super.instructions(bndFile); 134 return this; 135 } 136 137 @Override 138 @SuppressWarnings("PMD.AvoidCatchingGenericException") 139 protected <T extends Resource> Collection<T> 140 doProvide(ResourceRequest<T> requested) { 141 if (!requested.accepts(ManifestAttributesType)) { 142 return Collections.emptyList(); 143 } 144 try (var analyzer = new Analyzer(); 145 var jar = new aQute.bnd.osgi.Jar("dot")) { 146 // Assemble bundle content 147 var content = Resources.of(ClassTreesType).addAll(project() 148 .providers().resources(of(ClassTreeType).using(Supply))); 149 // A bnd ("better never document") Jar can actually be a 150 // classfile tree, and several such "Jar"s can be merged. 151 // IOException will be thrown (.get()) and handled in the outer try 152 vavrStream(content).find(_ -> true).peek(t -> Try.of(() -> jar 153 .addAll(new aQute.bnd.osgi.Jar(t.root().toFile()))).get()); 154 analyzer.setJar(jar); 155 applyInstructions(analyzer); 156 157 // Add classpath dependencies 158 var bundleDeps = Resources.of( 159 new ResourceType<Resources<LibraryJarFile>>() {}).addAll( 160 project().providers(Consume, Reveal, Expose) 161 .resources(project().of(LibraryJarFileType))); 162 logger.atFiner().log("BndAnalyzer in" 163 + " %s uses dependencies %s", project(), 164 lazy(() -> bundleDeps.stream().map(e -> e.path().toString()) 165 .collect(Collectors.joining(File.pathSeparator)))); 166 // IOException will be throw (.get()) and handled in the outer try 167 vavrStream(bundleDeps).forEach(dep -> Try 168 .run(() -> analyzer.addClasspath(dep.path().toFile())).get()); 169 170 // Evaluate and convert to result type 171 var manifest = analyzer.calcManifest(); 172 verifyManifest(manifest); 173 var asResource = ManifestAttributes.create(); 174 asResource.putAll(manifest.getMainAttributes()); 175 @SuppressWarnings("unchecked") 176 var result = (T) asResource; 177 return List.of(result); 178 } catch (Exception e) { 179 throw new BuildException().from(this).cause(e); 180 } 181 } 182 183 private void verifyManifest(Manifest manifest) { 184 Optional.ofNullable((String) manifest.getMainAttributes() 185 .get(new Attributes.Name("Bundle-Version"))).ifPresent(v -> { 186 try { 187 new Version(v); 188 } catch (IllegalArgumentException e) { 189 throw new ConfigurationException().message( 190 "Attempt to specify invalid OSGi version %s", v) 191 .from(this).cause(e); 192 } 193 }); 194 195 } 196}