aQute.bnd.osgi.Builder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of biz.aQute.bndlib Show documentation
Show all versions of biz.aQute.bndlib Show documentation
bndlib: A Swiss Army Knife for OSGi
The newest version!
package aQute.bnd.osgi;
import static aQute.bnd.exceptions.FunctionWithException.asFunction;
import static aQute.libg.re.Catalog.caseInsenstive;
import static aQute.libg.re.Catalog.eof;
import static aQute.libg.re.Catalog.lit;
import static aQute.libg.re.Catalog.or;
import static aQute.libg.re.Catalog.setAll;
import static java.util.stream.Collectors.toList;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.zip.ZipException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import aQute.bnd.cdi.CDIAnnotations;
import aQute.bnd.component.DSAnnotations;
import aQute.bnd.differ.DiffPluginImpl;
import aQute.bnd.header.Attrs;
import aQute.bnd.header.OSGiHeader;
import aQute.bnd.header.Parameters;
import aQute.bnd.help.instructions.BuilderInstructions;
import aQute.bnd.make.Make;
import aQute.bnd.make.MakeBnd;
import aQute.bnd.make.MakeCopy;
import aQute.bnd.make.component.ServiceComponent;
import aQute.bnd.maven.PomPropertiesResource;
import aQute.bnd.maven.PomResource;
import aQute.bnd.metatype.MetatypeAnnotations;
import aQute.bnd.osgi.Descriptors.PackageRef;
import aQute.bnd.osgi.Descriptors.TypeRef;
import aQute.bnd.osgi.metainf.MetaInfServiceMerger;
import aQute.bnd.osgi.metainf.MetaInfServiceParser;
import aQute.bnd.plugin.jpms.JPMSAnnotations;
import aQute.bnd.plugin.jpms.JPMSModuleInfoPlugin;
import aQute.bnd.plugin.jpms.JPMSMultiReleasePlugin;
import aQute.bnd.plugin.spi.SPIDescriptorGenerator;
import aQute.bnd.service.SignerPlugin;
import aQute.bnd.service.diff.Delta;
import aQute.bnd.service.diff.Diff;
import aQute.bnd.service.diff.Tree;
import aQute.bnd.service.diff.Type;
import aQute.bnd.service.merge.MergeResources;
import aQute.bnd.service.specifications.BuilderSpecification;
import aQute.bnd.stream.MapStream;
import aQute.bnd.unmodifiable.Maps;
import aQute.bnd.version.Version;
import aQute.lib.collections.Logic;
import aQute.lib.collections.MultiMap;
import aQute.lib.hex.Hex;
import aQute.lib.io.IO;
import aQute.lib.regex.PatternConstants;
import aQute.lib.strings.Strings;
import aQute.lib.zip.ZipUtil;
import aQute.libg.generics.Create;
import aQute.libg.re.RE;
/**
* Include-Resource: ( [name '=' ] file )+ Private-Package: package-decl ( ','
* package-decl )* Export-Package: package-decl ( ',' package-decl )*
* Import-Package: package-decl ( ',' package-decl )* @version $Revision: 1.27 $
*/
public class Builder extends Analyzer {
@SuppressWarnings("deprecation")
private static final String INCLUDERESOURCE_HEADERS = Constants.INCLUDERESOURCE + "|"
+ Constants.INCLUDE_RESOURCE;
private final static Logger logger = LoggerFactory.getLogger(Builder.class);
private final static Pattern IR_PATTERN = Pattern
.compile("[{]?-?@?(?:[^=]+=)?\\s*([^}!]+).*");
private final DiffPluginImpl differ = new DiffPluginImpl();
private Pattern xdoNotCopy = null;
private static final int SPLIT_MERGE_LAST = 1;
private static final int SPLIT_MERGE_FIRST = 2;
private static final int SPLIT_ERROR = 3;
private static final int SPLIT_FIRST = 4;
private static final int SPLIT_DEFAULT = 0;
private final List sourcePath = new ArrayList<>();
private final Make make = new Make(this);
private Instructions defaultPreProcessMatcher = null;
private BuilderInstructions buildInstrs = getInstructions(BuilderInstructions.class);
private final Map cachedSystemCalls;
public Builder(Processor parent) {
super(parent);
cachedSystemCalls = new ConcurrentHashMap<>();
}
public Builder(Builder parent) {
super(parent);
cachedSystemCalls = parent.cachedSystemCalls;
}
public Builder() {
cachedSystemCalls = new ConcurrentHashMap<>();
}
public Jar build() throws Exception {
logger.debug("build");
init();
if (isTrue(getProperty(NOBUNDLES)))
return null;
if (getProperty(CONDUIT) != null)
error("Specified " + CONDUIT + " but calls build() instead of builds() (might be a programmer error");
Jar dot = getBuildJar();
doExpand(dot);
doIncludeResources(dot);
doWab(dot);
// Check if we override the calculation of the
// manifest. We still need to calculated it because
// we need to have analyzed the classpath.
Manifest manifest = calcManifest();
String mf = getProperty(MANIFEST);
if (mf != null) {
File mff = getFile(mf);
if (mff.isFile()) {
updateModified(mff.lastModified(), "Manifest " + mff);
try (InputStream in = IO.stream(mff)) {
manifest = new Manifest(in);
} catch (Exception e) {
exception(e, "%s: exception while reading manifest file", MANIFEST);
}
} else {
error("%s: no such file %s", MANIFEST, mf);
}
}
if (!isTrue(getProperty(NOMANIFEST))) {
dot.setManifest(manifest);
String manifestName = getProperty(MANIFEST_NAME);
if (manifestName != null)
dot.setManifestName(manifestName);
} else {
dot.setDoNotTouchManifest();
}
// This must happen after we analyzed so
// we know what it is on the classpath
addSources(dot);
doPom(dot);
if (!isNoBundle())
doVerify(dot);
Map resources = dot.getResources();
if (resources.isEmpty() || ((resources.size() == 1) && resources.get("module-info.class") != null))
warning(
"The JAR is empty: The instructions for the JAR named %s did not cause any content to be included, this is likely wrong",
getBsn());
dot.updateModified(lastModified(), "Last Modified Processor");
dot.setName(getBsn());
doDigests(dot);
sign(dot);
doSaveManifest(dot);
doDiff(dot); // check if need to diff this bundle
doBaseline(dot); // check for a baseline
String expand = getProperty("-expand");
if (expand != null) {
File out = getFile(expand);
IO.mkdirs(out);
dot.expand(out);
}
return dot;
}
private Jar getBuildJar() {
Jar dot = getJar();
if (dot == null) {
dot = new Jar("dot");
setJar(dot);
}
buildInstrs.compression()
.ifPresent(dot::setCompression);
dot.setReproducible(getProperty(REPRODUCIBLE));
try {
long modified = Long.parseLong(getProperty("base.modified"));
dot.updateModified(modified, "Base modified");
} catch (Exception e) {
// Ignore
}
return dot;
}
void doPom(Jar dot) throws Exception, IOException {
try (Processor scoped = new Processor(this)) {
String bsn = getBsn();
if (bsn != null)
scoped.setProperty("@bsn", bsn);
String version = getBundleVersion();
if (version != null)
scoped.setProperty("@version", version);
String pom = scoped.getProperty(POM);
if (isTrue(pom)) {
dot.removeSubDirs("META-INF/maven/");
scoped.addProperties(OSGiHeader.parseProperties(pom));
PomResource pomXml = new PomResource(scoped, dot.getManifest());
String v = pomXml.validate();
if (v != null) {
error("Invalid pom for %s: %s", getBundleSymbolicName(), v);
}
PomPropertiesResource pomProperties = new PomPropertiesResource(pomXml);
dot.putResource(pomXml.getWhere(), pomXml);
if (!pomProperties.getWhere()
.equals(pomXml.getWhere())) {
dot.putResource(pomProperties.getWhere(), pomProperties);
}
}
}
}
/**
* Check if we need to calculate any checksums.
*
* @param dot
* @throws Exception
*/
private void doDigests(Jar dot) throws Exception {
Parameters ps = OSGiHeader.parseHeader(getProperty(DIGESTS));
if (ps.isEmpty())
return;
logger.debug("digests {}", ps);
String[] digests = ps.keySet()
.toArray(new String[0]);
dot.setDigestAlgorithms(digests);
}
/**
* Allow any local initialization by subclasses before we build.
*/
public void init() throws Exception {
begin();
doRequireBnd();
// Check if we have sensible setup
if (getClasspath().isEmpty() && (getProperty(EXPORT_PACKAGE) != null || getProperty(PRIVATE_PACKAGE) != null
|| getProperty(PRIVATEPACKAGE) != null))
warning("Classpath is empty. " + Constants.PRIVATE_PACKAGE + ", " + Constants.PRIVATEPACKAGE + ", and "
+ EXPORT_PACKAGE + " can only expand from the classpath when there is one");
}
/**
* Turn this normal bundle in a web and add any resources.
*
* @throws Exception
*/
private Jar doWab(Jar dot) throws Exception {
String wab = getProperty(WAB);
String wablib = getProperty(WABLIB);
if (wab == null && wablib == null)
return dot;
logger.debug("wab {} {}", wab, wablib);
setBundleClasspath(append("WEB-INF/classes", getProperty(BUNDLE_CLASSPATH)));
dot.getResources()
.keySet()
.stream()
.filter(path -> !pathStartsWith(path, "WEB-INF") && Constants.METAPACKAGES.stream()
.noneMatch(meta -> pathStartsWith(path, meta)))
// we collect since we need to mutate the source set
.collect(toList())
.forEach(path -> {
logger.debug("wab: moving: {}", path);
dot.rename(path, "WEB-INF/classes/" + path);
});
Parameters clauses = parseHeader(getProperty(WABLIB));
for (Map.Entry entry : clauses.entrySet()) {
File f = getFile(entry.getKey());
addWabLib(dot, f, entry.getKey(), entry.getValue());
}
doIncludeResource(dot, wab);
return dot;
}
private static boolean pathStartsWith(String path, String prefix) {
return path.startsWith(prefix) && ((path.length() == prefix.length()) || (path.charAt(prefix.length()) == '/'));
}
/**
* Add a wab lib to the jar.
*
* @param f
*/
private void addWabLib(Jar dot, File f, String name, Map attrs) throws Exception {
if (f.exists()) {
Jar jar = new Jar(f);
jar.setDoNotTouchManifest();
buildInstrs.compression()
.ifPresent(jar::setCompression);
addClose(jar);
String path = "WEB-INF/lib/" + f.getName();
dot.putResource(path, new JarResource(jar));
setProperty(BUNDLE_CLASSPATH, append(getProperty(BUNDLE_CLASSPATH), path));
Manifest m = jar.getManifest();
if (m != null) {
String cp = m.getMainAttributes()
.getValue("Class-Path");
if (cp != null) {
Collection parts = split(cp);
for (String part : parts) {
File sub = getFile(f.getParentFile(), part);
if (!sub.exists() || !sub.getParentFile()
.equals(f.getParentFile())) {
warning("Invalid Class-Path entry %s in %s, must exist and must reside in same directory",
sub, f);
} else {
addWabLib(dot, sub, part, Collections.emptyMap());
}
}
}
}
} else {
doIncludeResource(dot, name, attrs);
}
}
/**
* Get the manifest and write it out separately if -savemanifest is set
*
* @param dot
*/
private void doSaveManifest(Jar dot) throws Exception {
String output = getProperty(SAVEMANIFEST);
if (output == null)
return;
File f = getFile(output);
if (f.isDirectory()) {
f = new File(f, "MANIFEST.MF");
}
if (!f.exists() || f.lastModified() < dot.lastModified()) {
IO.delete(f);
File fp = f.getParentFile();
IO.mkdirs(fp);
try (OutputStream out = IO.outputStream(f)) {
Jar.writeManifest(dot.getManifest(), out);
}
changedFile(f);
}
}
protected void changedFile(@SuppressWarnings("unused")
File f) {}
/**
* Sign the jar file. -sign : [ ';' 'password:=' ] [ ';'
* 'keystore:=' ] [ ';' 'sign-password:=' ] ( ',' ... )*
*/
void sign(@SuppressWarnings("unused")
Jar jar) throws Exception {
String signing = getProperty(SIGN);
if (signing == null)
return;
logger.debug("Signing {}, with {}", getBsn(), signing);
List signers = getPlugins(SignerPlugin.class);
Parameters infos = parseHeader(signing);
for (String alias : infos.keySet()) {
for (SignerPlugin signer : signers) {
signer.sign(this, alias);
}
}
}
public boolean hasSources() {
return isTrue(getProperty(SOURCES));
}
/**
* Answer extra packages. In this case we implement conditional package. Any
*/
@Override
protected Jar getExtra() throws Exception {
Parameters conditionals = getMergedParameters(CONDITIONAL_PACKAGE);
conditionals.putAll(decorated(CONDITIONALPACKAGE));
if (conditionals.isEmpty())
return null;
logger.debug("do Conditional Package {}", conditionals);
Instructions instructions = new Instructions(conditionals);
Collection referred = instructions.select(getReferred().keySet(), false);
referred.removeAll(getContained().keySet());
if (referred.isEmpty()) {
logger.debug("no additional conditional packages to add");
return null;
}
Jar jar = new Jar(CONDITIONALPACKAGE);
addClose(jar);
for (PackageRef pref : referred) {
for (Jar cpe : getClasspath()) {
Map map = cpe.getDirectory(pref.getPath());
if (map != null) {
copy(jar, cpe, pref.getPath(), false);
break;
}
}
}
if (jar.getDirectories()
.isEmpty()) {
logger.debug("extra dirs {}", jar.getDirectories());
return null;
}
return jar;
}
/**
* Intercept the call to analyze and cleanup versions after we have analyzed
* the setup. We do not want to cleanup if we are going to verify.
*/
@Override
public void analyze() throws Exception {
super.analyze();
cleanupVersion(getImports(), null, Constants.IMPORT_PACKAGE);
cleanupVersion(getExports(), getVersion(), Constants.EXPORT_PACKAGE);
String version = getProperty(BUNDLE_VERSION);
if (version != null) {
version = cleanupVersion(version);
version = doSnapshot(version);
setProperty(BUNDLE_VERSION, version);
}
}
private String doSnapshot(String version) {
String snapshot = getProperty(SNAPSHOT);
if (snapshot == null) {
return version;
}
if (snapshot.isEmpty()) {
snapshot = null;
}
Version v = Version.parseVersion(version);
String q = v.getQualifier();
if (q == null) {
return version;
}
if (q.equals("SNAPSHOT")) {
q = snapshot;
} else if (q.endsWith("-SNAPSHOT")) {
int end = q.length() - "SNAPSHOT".length();
if (snapshot == null) {
q = q.substring(0, end - 1);
} else {
q = q.substring(0, end) + snapshot;
}
} else {
return version;
}
return new Version(v.getMajor(), v.getMinor(), v.getMicro(), q).toString();
}
public void cleanupVersion(Packages packages, String defaultVersion) {
cleanupVersion(packages, defaultVersion, "external");
}
public void cleanupVersion(Packages packages, String defaultVersion, String what) {
if (defaultVersion != null) {
Matcher m = Verifier.VERSION.matcher(defaultVersion);
if (m.matches()) {
// Strip qualifier from default package version
defaultVersion = Version.parseVersion(defaultVersion)
.toStringWithoutQualifier();
}
}
Set visited = new HashSet<>();
for (Map.Entry entry : packages.entrySet()) {
String packageName = Processor.removeDuplicateMarker(entry.getKey().fqn);
Attrs attributes = entry.getValue();
String v = attributes.get(Constants.VERSION_ATTRIBUTE);
if (v == null && defaultVersion != null) {
if (visited.contains(packageName)) {
SetLocation warning = warning(
"%s duplicate package name (%s) that uses the default version because no version is specified (%s). Remove duplicate package or add an explicit version to it.",
what, packageName, defaultVersion);
try {
getHeader(Constants.EXPORT_PACKAGE, entry.getKey().fqn);
} catch (Exception e) {
// not so important
}
}
if (!isTrue(getProperty(Constants.NODEFAULTVERSION))) {
v = defaultVersion;
if (isPedantic())
warning("Used bundle version %s for exported package %s", v, entry.getKey());
} else {
if (isPedantic())
warning("No export version for exported package %s", entry.getKey());
}
}
if (v != null)
attributes.put(Constants.VERSION_ATTRIBUTE, cleanupVersion(v));
visited.add(packageName);
}
}
private static final String[] fixed = {
"packageinfo", "package.html", "module-info.java", "package-info.java"
};
/**
* @throws IOException
*/
private void addSources(Jar dot) throws Exception {
if (!hasSources())
return;
Set packages = Create.set();
for (TypeRef typeRef : getClassspace().keySet()) {
PackageRef packageRef = typeRef.getPackageRef();
String sourcePath = typeRef.getSourcePath();
String packagePath = packageRef.getPath();
for (File root : getSourcePath()) {
File f = getFile(root, sourcePath);
if (f.exists()) {
if (!packages.contains(packageRef)) {
packages.add(packageRef);
for (String element : fixed) {
for (File sp : getSourcePath()) {
File bdir = getFile(sp, packagePath);
File ff = getFile(bdir, element);
if (ff.isFile()) {
String name = "OSGI-OPT/src/" + packagePath + "/" + element;
dot.putResource(name, new FileResource(ff));
break;
}
}
}
}
if (packageRef.isDefaultPackage())
logger.debug("Package reference is default package");
dot.putResource("OSGI-OPT/src/" + sourcePath, new FileResource(f));
}
}
if (getSourcePath().isEmpty())
warning("Including sources but " + SOURCEPATH + " does not contain any source directories ");
// TODO copy from the jars where they came from
}
}
boolean firstUse = true;
private Tree tree;
public Collection getSourcePath() {
if (firstUse) {
firstUse = false;
String sp = mergeProperties(SOURCEPATH);
if (sp != null) {
Parameters map = parseHeader(sp);
for (String file : map.keySet()) {
if (!isDuplicate(file)) {
File f = getFile(file);
if (!f.isDirectory()) {
error("Adding a sourcepath that is not a directory: %s", f).header(SOURCEPATH)
.context(file);
} else {
sourcePath.add(f);
}
}
}
}
}
return sourcePath;
}
private void doVerify(@SuppressWarnings("unused")
Jar dot) throws Exception {
// Give the verifier the benefit of our analysis
// prevents parsing the files twice
try (Verifier verifier = new Verifier(this)) {
verifier.setFrombuilder(true);
verifier.verify();
getInfo(verifier);
}
}
private void doExpand(Jar dot) throws Exception {
// Build an index of the class path that we can then
// use destructively
MultiMap packages = new MultiMap<>();
for (Jar srce : getClasspath()) {
dot.updateModified(srce.lastModified(), srce + " (" + srce.lastModifiedReason() + ")");
MapStream.of(srce.getDirectories())
.filterValue(Objects::nonNull)
.keys()
.forEachOrdered(path -> packages.add(path, srce));
}
Parameters private_package = getParameters(PRIVATE_PACKAGE);
Parameters privatepackage = decorated(PRIVATEPACKAGE);
Parameters testpackage = new Parameters();
Parameters includepackage = decorated(INCLUDEPACKAGE);
if (buildInstrs.undertest()) {
String h = mergeProperties(Constants.TESTPACKAGES, "test;presence:=optional");
testpackage = parseHeader(h);
}
Parameters includedPackages = new Parameters();
includedPackages.putAll(private_package);
includedPackages.putAll(privatepackage);
includedPackages.putAll(testpackage);
includedPackages.putAll(includepackage);
if (!includedPackages.isEmpty()) {
Instructions privateFilter = new Instructions(includedPackages);
Set unused = doExpand(dot, packages, privateFilter);
unused.removeIf(Instruction::isAny); // allow 0 matches
if (!unused.isEmpty()) {
String header = Constants.PRIVATEPACKAGE;
if (Logic.hasOverlap(private_package.keySet(), unused))
header = Constants.PRIVATE_PACKAGE;
else if (Logic.hasOverlap(testpackage.keySet(), unused))
header = Constants.TESTPACKAGES;
else if (Logic.hasOverlap(includepackage.keySet(), unused))
header = Constants.INCLUDEPACKAGE;
warning("Unused %s instructions , no such package(s) on the class path: %s", header, unused)
.header(header)
.context(unused.iterator()
.next()
.getInput());
}
}
Parameters exportedPackage = getExportPackage();
if (!exportedPackage.isEmpty()) {
Instructions exportedFilter = new Instructions(exportedPackage);
// We ignore unused instructions for exports, they should show
// up as errors during analysis. Otherwise any overlapping
// packages with the private packages should show up as
// unused
doExpand(dot, packages, exportedFilter);
}
}
/**
* Destructively filter the packages from the build up index. This index is
* used by the Export Package as well as the Private Package
*/
private Set doExpand(Jar jar, MultiMap index, Instructions filter) throws Exception {
Set unused = Create.set();
for (Entry e : filter.entrySet()) {
Instruction instruction = e.getKey();
if (instruction.isDuplicate())
continue;
Attrs directives = e.getValue();
String fromDirective = directives.get(FROM_DIRECTIVE, "*");
Instruction from = new Instruction(fromDirective);
boolean project = fromDirective.equals(FROM_DIRECTIVE_PROJECT);
boolean used = false;
for (Iterator>> entry = index.entrySet()
.iterator(); entry.hasNext();) {
Entry> p = entry.next();
String directory = p.getKey();
PackageRef packageRef = getPackageRef(directory);
// Skip * and meta data, we're talking packages!
if (packageRef.isMetaData() && instruction.isAny())
continue;
if (!instruction.matches(packageRef.getFQN()))
continue;
// Ensure it is never matched again
entry.remove();
// ! effectively removes it from consideration by others (this
// includes exports)
if (instruction.isNegated()) {
used = true;
continue;
}
// Do the from: directive, filters on the JAR type
List providers = filterFrom(from, p.getValue(), project);
if (providers.isEmpty())
continue;
int splitStrategy = getSplitStrategy(directives.get(SPLIT_PACKAGE_DIRECTIVE));
copyPackage(jar, providers, directory, splitStrategy);
Attrs contained = getContained().put(packageRef);
contained.put(INTERNAL_SOURCE_DIRECTIVE, getName(providers.get(0)));
used = true;
}
if (!used && !isTrue(directives.get("optional:")))
unused.add(instruction);
}
return unused;
}
/**
* @param from
*/
private List filterFrom(Instruction from, List providers, boolean project) {
if (from.isAny())
return providers;
if (project) {
for (Jar j : providers) {
if (j.getResource(PROJECT_MARKER) != null) {
return Collections.singletonList(j);
}
}
}
List np = new ArrayList<>();
for (Jar j : providers) {
if (from.matches(j.getName()) ^ from.isNegated()) {
np.add(j);
}
}
return np;
}
/**
* Copy the package from the providers based on the split package strategy.
*/
private void copyPackage(Jar dest, List providers, String path, int splitStrategy) {
switch (splitStrategy) {
case SPLIT_MERGE_LAST :
for (Jar srce : providers) {
copy(dest, srce, path, true);
}
break;
case SPLIT_MERGE_FIRST :
for (Jar srce : providers) {
copy(dest, srce, path, false);
}
break;
case SPLIT_ERROR :
error("%s", diagnostic(path, providers));
break;
case SPLIT_FIRST :
copy(dest, providers.get(0), path, false);
break;
default :
if (providers.size() > 1)
warning("%s", diagnostic(path, providers));
for (Jar srce : providers) {
copy(dest, srce, path, false);
}
break;
}
}
/**
* Copy
*/
private void copy(Jar dest, Jar srce, String path, boolean overwrite) {
logger.debug("copy d={} s={} p={}", dest, srce, path);
dest.copy(srce, path, overwrite);
if (hasSources()) {
dest.copy(srce, appendPath("OSGI-OPT/src", path), overwrite);
}
// bnd.info sources must be preprocessed
String bndInfoPath = appendPath(path, "bnd.info");
Resource r = dest.getResource(bndInfoPath);
if (r != null && !(r instanceof PreprocessResource)) {
logger.debug("preprocessing bnd.info");
PreprocessResource pp = new PreprocessResource(this, r);
dest.putResource(bndInfoPath, pp);
}
if (hasSources()) {
String srcPath = appendPath("OSGI-OPT/src", path);
Map srcContents = srce.getDirectory(srcPath);
if (srcContents != null) {
dest.addDirectory(srcContents, overwrite);
}
}
}
/**
* Analyze the classpath for a split package
*/
private String diagnostic(String pack, List culprits) {
// Default is like merge-first, but with a warning
return "Split package, multiple jars provide the same package:" + pack
+ "\nUse Import/Export Package directive -split-package:=(merge-first|merge-last|error|first) to get rid of this warning\n"
+ "Package found in " + culprits + "\n" //
+ "Class path " + getClasspath();
}
private int getSplitStrategy(String type) {
if (type == null)
return SPLIT_DEFAULT;
if (type.equals("merge-last"))
return SPLIT_MERGE_LAST;
if (type.equals("merge-first"))
return SPLIT_MERGE_FIRST;
if (type.equals("error"))
return SPLIT_ERROR;
if (type.equals("first"))
return SPLIT_FIRST;
error("Invalid strategy for split-package: %s", type);
return SPLIT_DEFAULT;
}
/**
* Matches the instructions against a package.
*
* @param instructions The list of instructions
* @param pack The name of the package
* @param unused The total list of patterns, matched patterns are removed
* @param source The name of the source container, can be filtered upon with
* the from: directive.
*/
private Instruction matches(Instructions instructions, String pack, Set unused, String source) {
for (Entry entry : instructions.entrySet()) {
Instruction pattern = entry.getKey();
// It is possible to filter on the source of the
// package with the from: directive. This is an
// instruction that must match the name of the
// source class path entry.
String from = entry.getValue()
.get(FROM_DIRECTIVE);
if (from != null) {
Instruction f = new Instruction(from);
if (!(f.matches(source) ^ f.isNegated()))
continue;
}
// Now do the normal
// matching
if (pattern.matches(pack)) {
if (unused != null)
unused.remove(pattern);
return pattern;
}
}
return null;
}
/**
* Parse the Bundle-Includes header. Files in the bundles Include header are
* included in the jar. The source can be a directory or a file.
*
* @throws IOException
* @throws FileNotFoundException
*/
@SuppressWarnings("deprecation")
private void doIncludeResources(Jar jar) throws Exception {
Parameters includes = parseHeader(getProperty("Bundle-Includes"));
if (includes.isEmpty()) {
includes = decorated(Constants.INCLUDERESOURCE);
includes.putAll(getMergedParameters(Constants.INCLUDE_RESOURCE));
} else {
warning("Please use -includeresource instead of Bundle-Includes");
}
doIncludeResource(jar, includes);
}
private void doIncludeResource(Jar jar, String includes) throws Exception {
Parameters clauses = parseHeader(includes);
doIncludeResource(jar, clauses);
}
private void doIncludeResource(Jar jar, Parameters clauses) throws ZipException, IOException, Exception {
for (Entry entry : clauses.entrySet()) {
String key = removeDuplicateMarker(entry.getKey());
doIncludeResource(jar, key, entry.getValue());
}
}
private void doIncludeResource(Jar jar, String name, Map extra)
throws ZipException, IOException, Exception {
Instructions preprocess = null;
boolean absentIsOk = false;
if (name.startsWith("{") && name.endsWith("}")) {
preprocess = getPreProcessMatcher(extra);
name = name.substring(1, name.length() - 1)
.trim();
}
String parts[] = name.split("\\s*=\\s*");
String source = parts[0];
String destination = parts[0];
if (parts.length == 2)
source = parts[1];
if (source.startsWith("-")) {
source = source.substring(1);
absentIsOk = true;
}
if (source.startsWith("@")) {
extractFromJar(jar, source.substring(1), parts.length == 1 ? "" : destination, absentIsOk, extra);
} else if (extra.containsKey("cmd")) {
doCommand(jar, source, destination, extra, preprocess, absentIsOk);
} else if (extra.containsKey(LITERAL_ATTRIBUTE)) {
String literal = extra.get(LITERAL_ATTRIBUTE);
Resource r = new EmbeddedResource(literal, 0L);
addExtra(r, extra.get("extra"));
copy(jar, name, r, extra);
if (preprocess != null) {
warning("Preprocessing does not work for literals: %s", name);
}
} else if (extra.containsKey(Constants.CLASS_ATTRIBUTE)) {
doClassAttribute(jar, name, extra, preprocess, absentIsOk);
} else {
File sourceFile;
String destinationPath;
sourceFile = getFile(source);
if (parts.length == 1) {
// Directories should be copied to the root
// but files to their file name ...
if (sourceFile.isDirectory())
destinationPath = "";
else
destinationPath = sourceFile.getName();
} else {
destinationPath = parts[0];
}
// Handle directories
if (sourceFile.isDirectory()) {
destinationPath = doResourceDirectory(jar, extra, preprocess, sourceFile, destinationPath);
return;
}
// destinationPath = checkDestinationPath(destinationPath);
if (!sourceFile.exists()) {
if (absentIsOk)
return;
noSuchFile(jar, name, extra, source, destinationPath);
} else
copy(jar, destinationPath, sourceFile, preprocess, extra);
}
}
private void doClassAttribute(Jar jar, String name, Map extra, Instructions preprocess,
boolean absentIsOk) throws Exception {
FileLine header = getHeader(INCLUDERESOURCE_HEADERS, Constants.CLASS_ATTRIBUTE);
String fqn = extra.get(Constants.CLASS_ATTRIBUTE);
TypeRef typeRef = getTypeRefFromFQN(fqn);
if (typeRef == null) {
header.set(warning(
"-includeresource entry uses 'class' attribute to refer to classpath but the reference '%s' is not a value type ref (fqn)",
fqn));
} else {
Clazz clazz = findClass(typeRef);
if (clazz == null) {
if (!absentIsOk) {
header.set(warning(
"-includeresource entry uses 'class' attribute to refer to classpath but the reference '%s' could not be found",
typeRef));
}
} else {
Resource r = clazz.getResource();
addExtra(r, extra.get("extra"));
copy(jar, name, r, extra);
if (preprocess != null) {
warning("Preprocessing does not work for class references: %s", name);
}
}
}
}
private void addExtra(Resource resource, String value) {
if (value == null) {
return;
}
String encoded = resource.getExtra();
byte[] extra = (encoded != null) ? Resource.decodeExtra(encoded) : null;
resource.setExtra(Resource.encodeExtra(ZipUtil.extraFieldFromString(extra, value)));
}
private Instructions getPreProcessMatcher(Map extra) {
if (defaultPreProcessMatcher == null) {
String preprocessmatchers = mergeProperties(PREPROCESSMATCHERS);
if (preprocessmatchers == null || preprocessmatchers.trim()
.length() == 0)
preprocessmatchers = Constants.DEFAULT_PREPROCESSS_MATCHERS;
defaultPreProcessMatcher = new Instructions(preprocessmatchers);
}
if (extra == null)
return defaultPreProcessMatcher;
String additionalMatchers = extra.get(PREPROCESSMATCHERS);
if (additionalMatchers == null)
return defaultPreProcessMatcher;
Instructions specialMatcher = new Instructions(additionalMatchers);
specialMatcher.putAll(defaultPreProcessMatcher);
return specialMatcher;
}
/**
* It is possible in Include-Resource to use a system command that generates
* the contents, this is indicated with {@code cmd} attribute. The command
* can be repeated for a number of source files with the {@code for}
* attribute which indicates a list of repetitions, often down with the
* {@link Macro#_lsa(String[])} or {@link Macro#_lsb(String[])} macro. The
* repetition will repeat the given command for each item. The @} macro can
* be used to replace the current item. If no {@code for} is given, the
* source is used as the only item. If the destination contains a macro,
* each iteration will create a new file, otherwise the destination name is
* used.
*
* @param jar
* @param source
* @param destination
* @param extra
* @param preprocess
* @param absentIsOk
* @throws Exception
*/
private void doCommand(Jar jar, String source, String destination, Map extra,
Instructions preprocess, boolean absentIsOk) throws Exception {
String repeat = extra.get("for"); // TODO constant
if (repeat == null)
repeat = source;
Collection requires = split(extra.get("requires"));
long lastModified = 0;
for (String required : requires) {
File file = getFile(required);
if (!file.exists()) {
error(Constants.INCLUDERESOURCE + ".cmd for %s, requires %s, but no such file %s", source, required,
file.getAbsoluteFile()).header(INCLUDERESOURCE_HEADERS);
} else
lastModified = findLastModifiedWhileOlder(file, lastModified());
}
String cmd = extra.get("cmd");
List paths = new ArrayList<>();
for (String item : Processor.split(repeat)) {
File f = IO.getFile(item);
traverse(paths, f);
}
CombinedResource cr = null;
if (!destination.contains("${@}")) {
cr = new CombinedResource();
cr.lastModified = lastModified;
}
setProperty("@requires", join(requires, " "));
try {
for (String item : paths) {
setProperty("@", item);
try {
String path = getReplacer().process(destination);
String command = getReplacer().process(cmd);
File file = getFile(item);
if (file.exists())
lastModified = Math.max(lastModified, file.lastModified());
CommandResource cmdresource = new CommandResource(command, this, lastModified, getBase());
Resource r = cmdresource;
// Turn this resource into a file resource
// so we execute the command now and catch its
// errors
FileResource fr = new FileResource(r);
addClose(fr);
r = fr;
if (preprocess != null && preprocess.matches(path))
r = new PreprocessResource(this, r);
if (cr == null)
jar.putResource(path, r);
else
cr.addResource(r);
} finally {
unsetProperty("@");
}
}
} finally {
unsetProperty("@requires");
}
// Add last so the correct modification date is used
// to update the modified time.
if (cr != null)
jar.putResource(destination, cr);
updateModified(lastModified, Constants.INCLUDERESOURCE + ": cmd");
}
private void traverse(List paths, File item) {
if (item.isDirectory()) {
for (File sub : IO.listFiles(item)) {
traverse(paths, sub);
}
} else if (item.isFile())
paths.add(IO.absolutePath(item));
else
paths.add(item.getName());
}
/**
* Check if a file or directory is older than the given time.
*
* @param file
* @param lastModified
*/
private long findLastModifiedWhileOlder(File file, long lastModified) {
if (file.isDirectory()) {
for (File child : IO.listFiles(file)) {
if (child.lastModified() > lastModified)
return child.lastModified();
long lm = findLastModifiedWhileOlder(child, lastModified);
if (lm > lastModified)
return lm;
}
}
return file.lastModified();
}
private String doResourceDirectory(Jar jar, Map extra, Instructions preprocess, File sourceFile,
String destinationPath) throws Exception {
String filter = extra.get("filter:");
boolean flatten = isTrue(extra.get("flatten:"));
boolean recursive = true;
String directive = extra.get("recursive:");
if (directive != null) {
recursive = isTrue(directive);
}
Instruction.Filter iFilter = null;
if (filter != null) {
iFilter = new Instruction.Filter(new Instruction(filter), recursive, getDoNotCopy());
} else {
iFilter = new Instruction.Filter(null, recursive, getDoNotCopy());
}
Map files = newMap();
resolveFiles(sourceFile, iFilter, recursive, destinationPath, files, flatten);
for (Map.Entry entry : files.entrySet()) {
copy(jar, entry.getKey(), entry.getValue(), preprocess, extra);
}
return destinationPath;
}
private void resolveFiles(File dir, FileFilter filter, boolean recursive, String path, Map files,
boolean flatten) {
if (doNotCopy(dir)) {
return;
}
List fs;
try (Stream names = IO.listStream(dir)) {
fs = names.map(name -> new File(dir, name))
.filter(filter::accept)
.collect(toList());
}
for (File file : fs) {
if (file.isDirectory()) {
if (recursive) {
String nextPath;
if (flatten)
nextPath = path;
else
nextPath = appendPath(path, file.getName());
resolveFiles(file, filter, recursive, nextPath, files, flatten);
}
// Directories are ignored otherwise
} else {
String p = appendPath(path, file.getName());
if (files.containsKey(p))
warning(Constants.INCLUDERESOURCE + " overwrites entry %s from file %s", p, file);
files.put(p, file);
}
}
if (fs.isEmpty()) {
File empty = new File(dir, Constants.EMPTY_HEADER);
files.put(appendPath(path, empty.getName()), empty);
}
}
private void noSuchFile(Jar jar, String clause, Map extra, String source, String destinationPath)
throws Exception {
List src = getJarsFromName(source, Constants.INCLUDERESOURCE + " " + source);
if (!src.isEmpty()) {
for (Jar j : src) {
File sourceFile = j.getSource();
String quoted = (sourceFile != null) ? sourceFile.getName() : j.getName();
Resource resource;
if ((sourceFile != null) && sourceFile.isFile()) {
resource = new FileResource(sourceFile);
} else {
// Do not touch the manifest so this also
// works for signed files.
j.setDoNotTouchManifest();
resource = new JarResource(j);
}
String path = destinationPath.replace(source, quoted);
logger.debug("copy d={} s={} path={}", jar, j, path);
copy(jar, path, resource, extra);
}
} else {
Resource lastChance = make.process(source);
if (lastChance != null) {
addExtra(lastChance, extra.get("extra"));
copy(jar, destinationPath, lastChance, extra);
} else
error("Input file does not exist: %s", source).header(source)
.context(clause);
}
}
final static RE ZIP_P = caseInsenstive(setAll, lit("."), or("jar", "zip"), eof);
/**
* Extra resources from a Jar and add them to the given jar.
*
* @param extra
*/
private void extractFromJar(Jar jar, String source, String destination, boolean absentIsOk,
Map extra) throws ZipException, IOException {
// Inline all resources and classes from another jar
// optionally appended with a modified regular expression
// like @zip.jar!/META-INF/MANIFEST.MF
int n = source.lastIndexOf("!/");
Instruction instr = null;
if (n > 0) {
instr = new Instruction(source.substring(n + 2));
source = source.substring(0, n);
}
File f = getFile(source);
if (f.isDirectory() && ZIP_P.matches(destination)
.isPresent()) {
DirectoryResource resource = new DirectoryResource(f);
jar.putResource(destination, resource);
return;
}
List sub = getJarsFromName(source, "extract from jar");
if (sub.isEmpty()) {
if (absentIsOk)
return;
error("Can not find JAR file '%s'", source);
} else {
Function nameMapper = Function.identity();
if (isTrue(extra.get("flatten:"))) {
nameMapper = path -> Strings.getLastSegment(path, '/');
} else if (instr != null) {
if (extra.containsKey("rename:")) {
Instruction in = instr;
String replacement = extra.get("rename:");
nameMapper = path -> in.getMatcher(path)
.replaceAll(replacement);
}
}
for (Jar j : sub)
addAll(jar, j, instr, destination, nameMapper, extra);
}
}
/**
* Add all the resources in the given jar that match the given filter.
*
* @param sub the jar
* @param filter a pattern that should match the resoures in sub to be added
*/
public boolean addAll(Jar to, Jar sub, Instruction filter) {
return addAll(to, sub, filter, "");
}
/**
* Add all the resources in the given jar that match the given filter.
*
* @param sub the jar
* @param filter a pattern that should match the resoures in sub to be added
*/
public boolean addAll(Jar to, Jar sub, Instruction filter, String destination) {
return addAll(to, sub, filter, destination, Function.identity(), Maps.of());
}
private boolean addAll(Jar to, Jar sub, Instruction filter, String destination, Function modifier,
Map extra) {
Function> dupStrategy = parseDupStrategy(extra);
boolean dupl = false;
for (String name : sub.getResources()
.keySet()) {
if (JarFile.MANIFEST_NAME.equals(name))
continue;
if (doNotCopy(Strings.getLastSegment(name, '/')))
continue;
if (filter == null || filter.matches(name) ^ filter.isNegated()) {
String path = Processor.appendPath(destination, modifier.apply(name));
Resource resource = sub.getResource(name);
Resource existing = to.getResource(path);
boolean duplicate = existing != null;
if (!duplicate) {
dupl |= to.putResource(path, resource, true);
} else {
Optional maybeMerged = dupStrategy.apply(new Duplication(path, existing, resource));
// Resource maybeMerged = dupStrategy.onDuplicate(path,
// existing, resource, this);
if (maybeMerged.isPresent()) {
dupl |= to.putResource(path, maybeMerged.get());
}
}
}
}
return dupl;
}
@SuppressWarnings({
"deprecation", "resource"
})
private void copy(Jar jar, String path, File from, Instructions preprocess, Map extra)
throws Exception {
if (doNotCopy(from))
return;
logger.debug("copy d={} s={} path={}", jar, from, path);
if (from.isDirectory()) {
for (File file : IO.listFiles(from)) {
copy(jar, appendPath(path, file.getName()), file, preprocess, extra);
}
} else {
if (from.exists()) {
Resource resource = new FileResource(from);
if (preprocess != null && preprocess.matches(path)) {
Processor tmp = new Processor(this);
addClose(tmp);
tmp.setProperty(".", from.getAbsolutePath()
.replace('\\', '/'));
resource = new PreprocessResource(tmp, resource);
}
addExtra(resource, extra.get("extra"));
if (path.endsWith("/"))
path = path + from.getName();
copy(jar, path, resource, extra);
} else if (from.getName()
.equals(Constants.EMPTY_HEADER)) {
jar.putResource(path, new EmbeddedResource(new byte[0], 0L));
} else {
error("Input file does not exist: %s", from).header(INCLUDERESOURCE + "|" + INCLUDE_RESOURCE);
}
}
}
private void copy(Jar jar, String path, Resource resource, Map extra) {
Resource existing = jar.getResource(path);
boolean duplicate = existing != null;
if (!duplicate) {
jar.putResource(path, resource, true);
} else {
Function> dupStrategy = parseDupStrategy(extra);
Optional maybeMerged = dupStrategy.apply(new Duplication(path, existing, resource));
if (maybeMerged.isPresent()) {
jar.putResource(path, maybeMerged.get(), true);
}
}
if (isTrue(extra.get(LIB_DIRECTIVE))) {
setProperty(BUNDLE_CLASSPATH, append(getProperty(BUNDLE_CLASSPATH, "."), path));
}
}
public void setSourcepath(File[] files) {
for (File file : files)
addSourcepath(file);
}
public void addSourcepath(File cp) {
if (!cp.exists())
warning("File on sourcepath that does not exist: %s", cp);
sourcePath.add(cp);
}
/**
* Build Multiple jars. If the -sub command is set, we filter the file with
* the given patterns.
*
* @throws Exception
*/
public Jar[] builds() throws Exception {
begin();
// Are we acting as a conduit for another JAR?
String conduit = getProperty(CONDUIT);
if (conduit != null) {
Parameters map = parseHeader(conduit);
Jar[] result = new Jar[map.size()];
int n = 0;
for (String file : map.keySet()) {
Jar c = new Jar(getFile(file));
c.setDoNotTouchManifest();
buildInstrs.compression()
.ifPresent(c::setCompression);
addClose(c);
String name = map.get(file)
.get("name");
if (name != null)
c.setName(name);
result[n++] = c;
}
return result;
}
List result = new ArrayList<>();
List builders;
builders = getSubBuilders();
for (Builder builder : builders) {
try {
startBuild(builder);
Jar jar = builder.build();
jar.setName(builder.getBsn());
result.add(jar);
doneBuild(builder);
} catch (Exception e) {
builder.exception(e, "Exception Building %s", builder.getBsn());
}
if (builder != this)
getInfo(builder, builder.getBsn() + ": ");
}
return result.toArray(new Jar[0]);
}
/**
* Called when we start to build a builder
*/
protected void startBuild(Builder builder) throws Exception {}
/**
* Called when we're done with a builder
*/
protected void doneBuild(Builder builder) throws Exception {}
/**
* Answer a list of builders that represent this file or a list of files
* specified in -sub. This list can be empty. These builders represents to
* be created artifacts and are each scoped to such an artifacts. The
* builders can be used to build the bundles or they can be used to find out
* information about the to be generated bundles.
*
* @return List of 0..n builders representing artifacts.
* @throws Exception
*/
public List getSubBuilders() throws Exception {
List builders = new ArrayList<>();
String sub = getProperty(SUB);
if (sub == null || sub.trim()
.length() == 0 || EMPTY_HEADER.equals(sub)) {
builders.add(this);
return builders;
}
if (isTrue(getProperty(NOBUNDLES)))
return builders;
Parameters subsMap = parseHeader(sub);
for (Iterator i = subsMap.keySet()
.iterator(); i.hasNext();) {
File file = getFile(i.next());
if (file.isFile() && !file.getName()
.startsWith(".")) {
builders.add(getSubBuilder(file));
i.remove();
}
}
Instructions instructions = new Instructions(subsMap);
List members = IO.listFiles(getBase());
nextFile: while (!members.isEmpty()) {
File file = members.remove(0);
// Check if the file is one of our parents
@SuppressWarnings("resource")
Processor p = this;
while (p != null) {
if (file.equals(p.getPropertiesFile()))
continue nextFile;
p = p.getParent();
}
for (Instruction instruction : instructions.keySet()) {
if (instruction.matches(file.getName())) {
if (!instruction.isNegated()) {
builders.add(getSubBuilder(file));
}
// Because we matched (even though we could be negated)
// we skip any remaining searches
continue nextFile;
}
}
}
return builders;
}
public Builder getSubBuilder(File file) throws Exception {
Builder builder = getSubBuilder();
if (builder != null) {
builder.setProperties(file);
addClose(builder);
}
return builder;
}
public Builder getSubBuilder() throws Exception {
Builder builder = new Builder(this);
builder.setBase(getBase());
builder.use(this);
for (Jar file : getClasspath()) {
builder.addClasspath(file);
}
return builder;
}
/**
* A macro to convert a maven version to an OSGi version
*/
public String _maven_version(String args[]) {
if (args.length > 2)
error("${maven_version} macro receives too many arguments %s", Arrays.toString(args));
else if (args.length < 2)
error("${maven_version} macro has no arguments, use ${maven_version;1.2.3-SNAPSHOT}");
else {
return cleanupVersion(args[1]);
}
return null;
}
public String _permissions(String args[]) {
return new PermissionGenerator(this, args).generate();
}
/**
*
*/
public void removeBundleSpecificHeaders() {
setForceLocal(BUNDLE_SPECIFIC_HEADERS);
}
/**
* Check if the given resource is in scope of this bundle. That is, it
* checks if the Include-Resource includes this resource or if it is a class
* file it is on the class path and the Export-Package or Private-Package
* include this resource.
*/
public boolean isInScope(Collection resources) throws Exception {
Parameters clauses = parseHeader(mergeProperties(Constants.EXPORT_PACKAGE));
clauses.putAll(parseHeader(mergeProperties(Constants.PRIVATE_PACKAGE)));
clauses.putAll(parseHeader(mergeProperties(Constants.PRIVATEPACKAGE)));
if (isTrue(getProperty(Constants.UNDERTEST))) {
clauses.putAll(parseHeader(mergeProperties(Constants.TESTPACKAGES, "test;presence:=optional")));
}
Stream ir = getIncludedResourcePrefixes();
Instructions instructions = new Instructions(clauses);
for (File r : resources) {
String cpEntry = getClasspathEntrySuffix(r);
if (cpEntry != null) {
if (cpEntry.equals("")) // Meaning we actually have a CPE
return true;
String pack = Descriptors.getPackage(cpEntry);
Instruction i = matches(instructions, pack, null, r.getName());
if (i != null)
return !i.isNegated();
}
// Check if this resource starts with one of the I-C header
// paths.
String path = IO.absolutePath(r);
return ir.anyMatch(path::startsWith);
}
return false;
}
/**
* Extra the paths for the directories and files that are used in the
* Include-Resource header.
*/
private Stream getIncludedResourcePrefixes() {
Stream prefixes = getIncludeResource().stream()
.filterValue(attrs -> !attrs.containsKey("literal"))
.keys()
.map(IR_PATTERN::matcher)
.filter(Matcher::matches)
.map(m -> m.group(1))
.map(this::getFile)
.map(IO::absolutePath);
return prefixes;
}
/**
* Answer the string of the resource that it has in the container. It is
* possible that the resource is a classpath entry. In that case an empty
* string is returned.
*
* @param resource The resource to look for
* @return A suffix on the classpath or "" if the resource is a class path
* entry
* @throws Exception
*/
public String getClasspathEntrySuffix(File resource) throws Exception {
for (Jar jar : getClasspath()) {
File source = jar.getSource();
if (source != null) {
String sourcePath = IO.absolutePath(source);
String resourcePath = IO.absolutePath(resource);
if (sourcePath.equals(resourcePath))
return ""; // Matches a classpath entry
if (resourcePath.startsWith(sourcePath)) {
// Make sure that the path name is translated correctly
// i.e. on Windows the \ must be translated to /
String filePath = resourcePath.substring(sourcePath.length() + 1);
return filePath;
}
}
}
return null;
}
/**
* doNotCopy The doNotCopy variable maintains a patter for files that should
* not be copied. There is a default {@link #DEFAULT_DO_NOT_COPY} but this
* ca be overridden with the {@link Constants#DONOTCOPY} property.
*/
public boolean doNotCopy(String v) {
return getDoNotCopy().matcher(v)
.matches();
}
public boolean doNotCopy(File from) {
if (doNotCopy(from.getName())) {
return true;
}
if (!since(About._3_1)) {
return false;
}
URI uri = getBaseURI().relativize(from.toURI());
return doNotCopy(uri.getPath());
}
public Pattern getDoNotCopy() {
if (xdoNotCopy == null) {
String string = null;
try {
string = mergeProperties(DONOTCOPY);
if (string == null || string.isEmpty())
string = DEFAULT_DO_NOT_COPY;
xdoNotCopy = Pattern.compile(string);
} catch (Exception e) {
error("Invalid value for %s, value is %s", DONOTCOPY, string).header(DONOTCOPY);
xdoNotCopy = Pattern.compile(DEFAULT_DO_NOT_COPY);
}
}
return xdoNotCopy;
}
/**
*/
static MakeBnd makeBnd = new MakeBnd();
static MakeCopy makeCopy = new MakeCopy();
static ServiceComponent serviceComponent = new ServiceComponent();
static CDIAnnotations cdiAnnotations = new CDIAnnotations();
static DSAnnotations dsAnnotations = new DSAnnotations();
static MetatypeAnnotations metatypeAnnotations = new MetatypeAnnotations();
static JPMSAnnotations moduleAnnotations = new JPMSAnnotations();
static JPMSModuleInfoPlugin moduleInfoPlugin = new JPMSModuleInfoPlugin();
static SPIDescriptorGenerator spiDescriptorGenerator = new SPIDescriptorGenerator();
static JPMSMultiReleasePlugin jpmsReleasePlugin = new JPMSMultiReleasePlugin();
static MetaInfServiceParser metaInfoServiceParser = new MetaInfServiceParser();
static MetaInfServiceMerger metaInfServiceMerger = new MetaInfServiceMerger();
@Override
protected void setTypeSpecificPlugins(PluginsContainer pluginsContainer) {
pluginsContainer.add(makeBnd);
pluginsContainer.add(makeCopy);
pluginsContainer.add(serviceComponent);
pluginsContainer.add(cdiAnnotations);
pluginsContainer.add(dsAnnotations);
pluginsContainer.add(metatypeAnnotations);
pluginsContainer.add(moduleAnnotations);
pluginsContainer.add(moduleInfoPlugin);
pluginsContainer.add(spiDescriptorGenerator);
pluginsContainer.add(jpmsReleasePlugin);
pluginsContainer.add(metaInfoServiceParser);
pluginsContainer.add(metaInfServiceMerger);
super.setTypeSpecificPlugins(pluginsContainer);
}
/**
* Diff this bundle to another bundle for the given packages.
*
* @throws Exception
*/
public void doDiff(@SuppressWarnings("unused")
Jar dot) throws Exception {
Parameters diffs = parseHeader(getProperty("-diff"));
if (diffs.isEmpty())
return;
logger.debug("diff {}", diffs);
if (tree == null)
tree = differ.tree(this);
for (Entry entry : diffs.entrySet()) {
String path = entry.getKey();
File file = getFile(path);
if (!file.isFile()) {
error("Diffing against %s that is not a file", file).header("-diff")
.context(path);
continue;
}
boolean full = entry.getValue()
.get("--full") != null;
boolean warning = entry.getValue()
.get("--warning") != null;
Tree other = differ.tree(file);
Diff api = tree.diff(other)
.get("");
Instructions instructions = new Instructions(entry.getValue()
.get("--pack"));
logger.debug("diff against {} --full={} --pack={} --warning={}", file, full, instructions, warning);
for (Diff p : api.getChildren()) {
String pname = p.getName();
if (p.getType() == Type.PACKAGE && instructions.matches(pname)) {
if (p.getDelta() != Delta.UNCHANGED) {
if (!full)
if (warning)
warning("Differ %s", p).header("-diff")
.context(path);
else
error("Differ %s", p).header("-diff")
.context(path);
else {
if (warning)
warning("Diff found a difference in %s for packages %s", file, instructions)
.header("-diff")
.context(path);
else
error("Diff found a difference in %s for packages %s", file, instructions)
.header("-diff")
.context(path);
show(p, "", warning);
}
}
}
}
}
}
/**
* Show the diff recursively
*/
private void show(Diff p, String indent, boolean warning) {
Delta d = p.getDelta();
if (d == Delta.UNCHANGED)
return;
if (warning)
warning("%s%s", indent, p).header("-diff");
else
error("%s%s", indent, p).header("-diff");
indent = indent + " ";
switch (d) {
case CHANGED :
case MAJOR :
case MINOR :
case MICRO :
break;
default :
return;
}
for (Diff c : p.getChildren())
show(c, indent, warning);
}
public void addSourcepath(Collection sourcepath) {
for (File f : sourcepath) {
addSourcepath(f);
}
}
/**
* Base line against a previous version. Should be overridden in the
* ProjectBuilder where we have access to the repos
*
* @throws Exception
*/
protected void doBaseline(Jar dot) throws Exception {}
/**
* #388 Manifest header to get GIT head Get the head commit number. Look for
* a .git/HEAD file, going up in the file hierarchy. Then get this file, and
* resolve any symbolic reference.
*/
private final static Pattern GITREF_P = Pattern.compile("ref:\\s*(refs/(heads|tags|remotes)/(\\S+))\\s*");
private final static Pattern GIT_WORKTREES_P = Pattern.compile("gitdir:\\s*(\\S+)\\s*");
final static String _githeadHelp = "${githead}, provide the SHA for the current git head";
public String _githead(String[] args) throws IOException {
Macro.verifyCommand(args, _githeadHelp, null, 1, 1);
//
// Locate the .git directory
//
File GIT_DIR = null;
for (File rover = getBase(); (rover != null) && rover.isDirectory(); rover = rover.getParentFile()) {
GIT_DIR = IO.getFile(rover, ".git");
if (GIT_DIR.exists()) {
while (GIT_DIR.isFile()) {
String content = IO.collect(GIT_DIR)
.trim();
Matcher m = GIT_WORKTREES_P.matcher(content);
if (!m.matches()) {
break;
}
GIT_DIR = IO.getFile(rover, m.group(1));
}
break;
}
}
if ((GIT_DIR == null) || !GIT_DIR.isDirectory()) {
// Cannot find git directory
return "";
}
File GIT_COMMON_DIR = GIT_DIR;
File commondir = IO.getFile(GIT_DIR, "commondir");
if (commondir.isFile()) {
String content = IO.collect(commondir)
.trim();
GIT_COMMON_DIR = IO.getFile(GIT_DIR, content);
}
File HEAD = IO.getFile(GIT_DIR, "HEAD");
if (!HEAD.isFile()) {
// Cannot find HEAD file
return "";
}
//
// The head is either a SHA1 or symref (ref:
// refs/(heads|tags|remotes)/)
//
String content = IO.collect(HEAD)
.trim();
if (!Hex.isHex(content)) {
//
// Should be a symref
//
Matcher m = GITREF_P.matcher(content);
if (m.matches()) {
String symref = m.group(1);
// so the commit is in the following path
File file = IO.getFile(GIT_COMMON_DIR, symref);
if (!file.isFile()) {
// sigh, gc'd. Is in .git/packed-refs
file = IO.getFile(GIT_COMMON_DIR, "packed-refs");
if (file.isFile()) {
String refs = IO.collect(file);
Pattern packedReferenceLinePattern = Pattern
.compile("(" + PatternConstants.SHA1 + ")\\s+" + symref + "\\s*\n");
Matcher packedReferenceMatcher = packedReferenceLinePattern.matcher(refs);
if (packedReferenceMatcher.find()) {
content = packedReferenceMatcher.group(1);
} else {
return ""; // give up
}
} else {
return ""; // give up
}
} else {
content = IO.collect(file);
}
} else {
error(
"Git repo seems corrupt. It exists, find the HEAD but the content is neither hex nor a sym-ref: %s",
content);
}
}
return content.trim()
.toUpperCase(Locale.ROOT);
}
/**
* Create a report of the settings
*
* @throws Exception
*/
@Override
public void report(Map table) throws Exception {
build();
super.report(table);
table.put("Do Not Copy", getDoNotCopy());
table.put("Git head", _githead(new String[] {
"githead"
}));
}
/**
* Collect the information from the {@link BuilderSpecification}
*
* @throws IOException
*/
public Builder from(BuilderSpecification spec) throws IOException {
if (spec.bundleActivator != null)
setBundleActivator(spec.bundleActivator);
setFailOk(spec.failOk);
setSources(spec.sources);
setProperty(Constants.RESOURCEONLY, spec.resourceOnly + "");
if (!spec.bundleNativeCode.isEmpty())
setProperty(Constants.BUNDLE_NATIVECODE, new Parameters(spec.bundleNativeCode).toString());
if (!spec.bundleSymbolicName.isEmpty()) {
setBundleSymbolicName(new Parameters(spec.bundleSymbolicName).toString());
}
if (!spec.fragmentHost.isEmpty()) {
setProperty(Constants.FRAGMENT_HOST, new Parameters(spec.fragmentHost).toString());
}
if (spec.bundleVersion != null) {
setBundleVersion(spec.bundleVersion);
}
for (String path : spec.classpath) {
this.addClasspath(new File(path));
}
if (!spec.exportContents.isEmpty()) {
setProperty(Constants.EXPORT_CONTENTS, new Parameters(spec.exportContents).toString());
}
if (!spec.exportPackage.isEmpty()) {
setProperty(Constants.EXPORT_PACKAGE, new Parameters(spec.exportPackage).toString());
}
if (!spec.importPackage.isEmpty()) {
setProperty(Constants.IMPORT_PACKAGE, new Parameters(spec.importPackage).toString());
}
if (!spec.includeresource.isEmpty()) {
setProperty(Constants.INCLUDERESOURCE, new Parameters(spec.includeresource).toString());
}
if (!spec.privatePackage.isEmpty()) {
setProperty(Constants.PRIVATEPACKAGE, new Parameters(spec.privatePackage).toString());
}
if (!spec.provideCapability.isEmpty()) {
setProperty(Constants.PROVIDE_CAPABILITY, new Parameters(spec.provideCapability).toString());
}
if (!spec.requireBundle.isEmpty()) {
setProperty(Constants.REQUIRE_BUNDLE, new Parameters(spec.requireBundle).toString());
}
if (!spec.requireCapability.isEmpty()) {
setProperty(Constants.REQUIRE_CAPABILITY, new Parameters(spec.requireCapability).toString());
}
spec.other.forEach(this::setProperty);
return this;
}
/**
* We override system so that, for the duration of a build operation, we may
* cache the result of a given system call. In a large build with many
* 'make' build jars, such as some OSGi CT build projects, we may call
* system hundreds of time to compute headers based upon git information
* such as 'git describe'. Since this information will not change during the
* course of a single build operation, we cache results to only call once.
*/
@Override
public String system(boolean allowFail, String command, String input) throws IOException, InterruptedException {
Integer key = Objects.hash(command, input);
return cachedSystemCalls.computeIfAbsent(key, asFunction(k -> super.system(allowFail, command, input)));
}
private Function> parseDupStrategy(Map extra) {
String onduplicate = extra.get(DUP_STRATEGY);
return Duplication.doDuplicate(onduplicate, this);
}
/**
* Handles how duplicate resources are handled in -includeresource
* instruction.
*/
record Duplication(String path, Resource existing, Resource candidate) {
private static final String DUP_MSG_DEFAULT_OVERWRITE = "includeresource.duplicates: Duplicate overwritten: %s (Consider using the %s directive to handle duplicates.)";
enum OnDuplicateCommand {
OVERWRITE,
SKIP,
MERGE,
WARN,
ERROR;
}
public static Function> doDuplicate(String onduplicate, Processor processor) {
if (onduplicate == null || onduplicate.isBlank()) {
Consumer warn = dupl -> {
// but only warn if resources are not identical
if (!isIdentical(dupl.existing, dupl.candidate)) {
processor.warning(DUP_MSG_DEFAULT_OVERWRITE, dupl.path, Constants.DUP_STRATEGY);
}
};
return dupl -> {
warn.accept(dupl);
return Optional.of(dupl.candidate);
};
}
Set commands = new LinkedHashSet<>();
List tags = new ArrayList();
Strings.split(onduplicate)
.forEach(string -> {
try {
commands.add(OnDuplicateCommand.valueOf(string));
} catch (Exception e) {
tags.add(string);
}
});
Consumer error = commands.remove(OnDuplicateCommand.ERROR)
? dupl -> processor.error("includeresource.duplicates: duplicate found for path %s", dupl.path)
: d -> {};
Consumer warn = commands.remove(OnDuplicateCommand.WARN)
? dupl -> processor.warning("includeresource.duplicates: duplicate found for path %s", dupl.path)
: d -> {};
String[] tags2 = tags.toArray(new String[0]);
Function> result;
int what = tags.isEmpty() ? 0 : 1;
if (!commands.isEmpty())
what += 2;
result = switch (what) {
// OVERWRITE
case 0 -> dupl -> Optional.of(dupl.candidate);
// try MERGE
case 1 -> {
List mergers = mergePlugins(tags2, processor);
yield dupl -> merge(dupl, mergers);
}
// commands
case 2, 3 -> getCommand(commands, tags2, processor);
default -> throw new UnsupportedOperationException();
};
return dupl -> {
error.accept(dupl);
warn.accept(dupl);
return result.apply(dupl);
};
}
private static Function> getCommand(Set commands,
String[] tags, Processor processor) {
if (commands.size() != 1) {
processor.error("includeresource.duplicates: specifies multiple strategies to handle duplicates: %s",
commands);
return dupl -> Optional.empty();
}
if (commands.contains(OnDuplicateCommand.OVERWRITE)) {
return dupl -> Optional.of(dupl.candidate);
} else if (commands.contains(OnDuplicateCommand.SKIP)) {
return dupl -> Optional.empty();
} else if (commands.contains(OnDuplicateCommand.MERGE)) {
return dupl -> merge(dupl, mergePlugins(tags, processor));
} else {
throw new UnsupportedOperationException("missed an enum value? " + commands);
}
}
private static List mergePlugins(String[] tags, Processor processor) {
List plugins = processor.getPlugins(MergeResources.class, tags);
if (tags.length > 0 && plugins.isEmpty()) {
processor.error("includeresource.duplicates: no plugins found for tags: %s", Arrays.toString(tags));
}
return plugins;
}
private static Optional merge(Duplication dupl, List list) {
for (MergeResources mr : list) {
Optional merged = mr.tryMerge(dupl.path, dupl.existing, dupl.candidate);
if (merged.isPresent())
return merged;
}
return Optional.empty();
}
private static boolean isIdentical(Resource a, Resource b) {
try {
ByteBuffer buffer1 = a.buffer();
ByteBuffer buffer2 = b.buffer();
if (buffer1.remaining() != buffer2.remaining()) {
return false;
}
return buffer1.equals(buffer2);
} catch (Exception e) {
return false;
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy