blob: 42dd7a16cc8f4f376b9746af70c005858435a5df [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.felix.bundleplugin;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipFile;
import aQute.bnd.osgi.Analyzer;
import aQute.bnd.osgi.Builder;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Instructions;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Resource;
import aQute.lib.collections.ExtList;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.Scanner;
import org.osgi.service.metatype.MetaTypeService;
import org.sonatype.plexus.build.incremental.BuildContext;
import static aQute.lib.strings.Strings.join;
/**
* Generate an OSGi manifest for this project
*/
@Mojo( name = "manifest", requiresDependencyResolution = ResolutionScope.TEST,
threadSafe = true,
defaultPhase = LifecyclePhase.PROCESS_CLASSES)
public class ManifestPlugin extends BundlePlugin
{
/**
* When true, generate the manifest by rebuilding the full bundle in memory
*/
@Parameter( property = "rebuildBundle" )
protected boolean rebuildBundle;
/**
* When true, manifest generation on incremental builds is supported in IDEs like Eclipse.
* Please note that the underlying BND library does not support incremental build, which means
* always the whole manifest and SCR metadata is generated.
*/
@Parameter( property = "supportIncrementalBuild" )
private boolean supportIncrementalBuild;
/**
* When true, show stale files in the log at info level else at debug level.
*/
@Parameter( property = "showStaleFiles" )
private boolean showStaleFiles;
@Component
private BuildContext buildContext;
@Override
protected void execute( Map<String, String> instructions, ClassPathItem[] classpath )
throws MojoExecutionException
{
File outputFile = new File( manifestLocation, "MANIFEST.MF" );
boolean metadataUpToDate = isMetadataUpToDate(outputFile, project);
if (supportIncrementalBuild && metadataUpToDate && isUpToDate(project)) {
return;
}
// in incremental build execute manifest generation only when explicitly activated
// and when any java file was touched since last build
if (buildContext.isIncremental() && (!supportIncrementalBuild //
|| (metadataUpToDate && !anyJavaSourceFileTouchedSinceLastBuild())))
{
getLog().debug("Skipping manifest generation because no java source file was added, updated or removed since last build.");
return;
}
Analyzer analyzer;
try
{
analyzer = getAnalyzer(project, instructions, classpath);
}
catch ( FileNotFoundException e )
{
throw new MojoExecutionException( "Cannot find " + e.getMessage()
+ " (manifest goal must be run after compile phase)", e );
}
catch ( IOException e )
{
getLog().error( e.getLocalizedMessage() );
throw new MojoExecutionException( "Error trying to generate Manifest", e );
}
catch ( MojoFailureException e )
{
getLog().error( e.getLocalizedMessage() );
throw new MojoExecutionException( "Error(s) found in manifest configuration", e );
}
catch ( Exception e )
{
getLog().error( "An internal error occurred", e );
throw new MojoExecutionException( "Internal error in maven-bundle-plugin", e );
}
try
{
writeManifest( analyzer, outputFile, niceManifest, exportScr, scrLocation, buildContext, getLog() );
if (supportIncrementalBuild) {
writeIncrementalInfo(project);
}
}
catch ( Exception e )
{
throw new MojoExecutionException( "Error trying to write Manifest to file " + outputFile, e );
}
finally
{
try
{
analyzer.close();
}
catch ( IOException e )
{
throw new MojoExecutionException( "Error trying to write Manifest to file " + outputFile, e );
}
}
}
/**
* Checks if any *.java file was added, updated or removed since last build in any source directory.
*/
private boolean anyJavaSourceFileTouchedSinceLastBuild() {
@SuppressWarnings("unchecked")
List<String> sourceDirectories = project.getCompileSourceRoots();
for (String sourceDirectory : sourceDirectories) {
File directory = new File(sourceDirectory);
Scanner scanner = buildContext.newScanner(directory);
Scanner deleteScanner = buildContext.newDeleteScanner(directory);
if (containsJavaFile(scanner) || containsJavaFile(deleteScanner)) {
return true;
}
}
return false;
}
private boolean containsJavaFile(Scanner scanner) {
String[] includes = new String[] { "**/*.java" };
scanner.setIncludes(includes);
scanner.scan();
return scanner.getIncludedFiles().length > 0;
}
public Manifest getManifest( MavenProject project, ClassPathItem[] classpath ) throws IOException, MojoFailureException,
MojoExecutionException, Exception
{
return getManifest( project, new LinkedHashMap<String, String>(), classpath, buildContext );
}
public Manifest getManifest( MavenProject project, Map<String, String> instructions, ClassPathItem[] classpath,
BuildContext buildContext ) throws IOException, MojoFailureException, MojoExecutionException, Exception
{
Analyzer analyzer = getAnalyzer(project, instructions, classpath);
Jar jar = analyzer.getJar();
Manifest manifest = jar.getManifest();
if (exportScr)
{
exportScr(analyzer, jar, scrLocation, buildContext, getLog() );
}
// cleanup...
analyzer.close();
return manifest;
}
private static void exportScr(Analyzer analyzer, Jar jar, File scrLocation, BuildContext buildContext, Log log ) throws Exception {
log.debug("Export SCR metadata to: " + scrLocation.getPath());
scrLocation.mkdirs();
// export SCR metadata files from OSGI-INF/
Map<String, Resource> scrDir = jar.getDirectories().get("OSGI-INF");
if (scrDir != null) {
for (Map.Entry<String, Resource> entry : scrDir.entrySet()) {
String path = entry.getKey();
Resource resource = entry.getValue();
writeSCR(resource, new File(scrLocation, path), buildContext,
log);
}
}
// export metatype files from OSGI-INF/metatype
Map<String,Resource> metatypeDir = jar.getDirectories().get(MetaTypeService.METATYPE_DOCUMENTS_LOCATION);
if (metatypeDir != null) {
for (Map.Entry<String, Resource> entry : metatypeDir.entrySet())
{
String path = entry.getKey();
Resource resource = entry.getValue();
writeSCR(resource, new File(scrLocation, path), buildContext, log);
}
}
}
private static void writeSCR(Resource resource, File destination, BuildContext buildContext, Log log ) throws Exception
{
log.debug("Write SCR file: " + destination.getPath());
destination.getParentFile().mkdirs();
OutputStream os = buildContext.newFileOutputStream(destination);
try
{
resource.write(os);
}
finally
{
os.close();
}
}
protected Analyzer getAnalyzer( MavenProject project, ClassPathItem[] classpath ) throws IOException, MojoExecutionException,
Exception
{
return getAnalyzer( project, new LinkedHashMap<>(), classpath );
}
protected Analyzer getAnalyzer( MavenProject project, Map<String, String> instructions, ClassPathItem[] classpath )
throws IOException, MojoExecutionException, Exception
{
if ( rebuildBundle && supportedProjectTypes.contains( project.getArtifact().getType() ) )
{
return buildOSGiBundle( project, instructions, classpath );
}
File file = getOutputDirectory();
if ( file == null )
{
file = project.getArtifact().getFile();
}
if ( !file.exists() )
{
if ( file.equals( getOutputDirectory() ) )
{
file.mkdirs();
}
else
{
throw new FileNotFoundException( file.getPath() );
}
}
Builder analyzer = getOSGiBuilder( project, instructions, classpath );
analyzer.setJar( file );
// calculateExportsFromContents when we have no explicit instructions defining
// the contents of the bundle *and* we are not analyzing the output directory,
// otherwise fall-back to addMavenInstructions approach
boolean isOutputDirectory = file.equals( getOutputDirectory() );
if ( analyzer.getProperty( Analyzer.EXPORT_PACKAGE ) == null
&& analyzer.getProperty( Analyzer.EXPORT_CONTENTS ) == null
&& analyzer.getProperty( Analyzer.PRIVATE_PACKAGE ) == null && !isOutputDirectory )
{
String export = calculateExportsFromContents( analyzer.getJar() );
analyzer.setProperty( Analyzer.EXPORT_PACKAGE, export );
}
addMavenInstructions( project, analyzer );
// if we spot Embed-Dependency and the bundle is "target/classes", assume we need to rebuild
if ( analyzer.getProperty( DependencyEmbedder.EMBED_DEPENDENCY ) != null && isOutputDirectory )
{
analyzer.build();
}
else
{
// FELIX-6495: workaround BND inconsistency: internal jar does not take "-reproducible" flag into account...
analyzer.getJar().setReproducible( "true".equals( analyzer.getProperties().getProperty( Constants.REPRODUCIBLE ) ) );
analyzer.mergeManifest( analyzer.getJar().getManifest() );
analyzer.getJar().setManifest( analyzer.calcManifest() );
}
mergeMavenManifest( project, analyzer );
boolean hasErrors = reportErrors( "Manifest " + project.getArtifact(), analyzer );
if ( hasErrors )
{
String failok = analyzer.getProperty( "-failok" );
if ( null == failok || "false".equalsIgnoreCase( failok ) )
{
throw new MojoFailureException( "Error(s) found in manifest configuration" );
}
}
Jar jar = analyzer.getJar();
if ( unpackBundle )
{
File outputFile = getOutputDirectory();
for ( Entry<String, Resource> entry : jar.getResources().entrySet() )
{
File entryFile = new File( outputFile, entry.getKey() );
if ( !entryFile.exists() || entry.getValue().lastModified() == 0 )
{
entryFile.getParentFile().mkdirs();
OutputStream os = buildContext.newFileOutputStream( entryFile );
entry.getValue().write( os );
os.close();
}
}
}
return analyzer;
}
private void writeIncrementalInfo(MavenProject project) throws MojoExecutionException {
try {
Path cacheData = getIncrementalDataPath(project);
String curdata = getIncrementalData();
Files.createDirectories(cacheData.getParent());
try (Writer w = Files.newBufferedWriter(cacheData)) {
w.append(curdata);
}
} catch (IOException e) {
throw getManifestUptodateCheckException(e);
}
}
private boolean isUpToDate(MavenProject project) throws MojoExecutionException {
try {
Path cacheData = getIncrementalDataPath(project);
String prvdata;
if (Files.isRegularFile(cacheData)) {
prvdata = new String(Files.readAllBytes(cacheData), StandardCharsets.UTF_8);
} else {
prvdata = null;
}
String curdata = getIncrementalData();
if (curdata.equals(prvdata)) {
long lastmod = lastModified(cacheData);
Set<String> stale = Stream.concat(Stream.of(new File(project.getBuild().getOutputDirectory())),
project.getArtifacts().stream().map(Artifact::getFile))
.flatMap(f -> newer(lastmod, f))
.collect(Collectors.toSet());
if (!stale.isEmpty()) {
getLog().info("Stale files detected, re-generating manifest.");
if (showStaleFiles) {
getLog().info("Stale files: " + join(", ", stale));
} else if (getLog().isDebugEnabled()) {
getLog().debug("Stale files: " + join(", ", stale));
}
} else {
// everything is in order, skip
getLog().info("Skipping manifest generation, everything is up to date.");
return true;
}
} else {
if (prvdata == null) {
getLog().info("No previous run data found, generating manifest.");
} else {
getLog().info("Configuration changed, re-generating manifest.");
}
}
} catch (IOException e) {
throw getManifestUptodateCheckException(e);
}
return false;
}
private boolean isMetadataUpToDate(File outputFile, MavenProject project)
throws MojoExecutionException
{
if (!outputFile.isFile()) // does MANIFEST.MF exist?
{
getLog().info("No MANIFEST.MF file found, generating manifest.");
return false;
}
try
{ // has this project's or a parent's pom.xml been modified after the manifest was
// generated last?
Path cacheData = getIncrementalDataPath(project);
if(!Files.isRegularFile(cacheData)) {
getLog().debug("No cache data file found at '" + cacheData + "', generating manifest.");
return false;
}
long manifestLastModified = lastModified(cacheData);
while (project != null)
{
if (project.getFile() != null) {
Path pom = project.getFile().toPath();
if (manifestLastModified < lastModified(pom))
{
getLog().debug("POM file at '" + pom + "' newer than cache data file, generating manifest.");
return false;
}
} else {
if (project.getVersion().endsWith("-SNAPSHOT")) { // is it mutable?
getLog().debug("POM file not accessible for SNAPSHOT project '" + project + "', assume modification.");
return false;
} else {
getLog().debug("POM file not accessible for non-SNAPSHOT project '" + project + "', assume no modification.");
}
}
project = project.getParent();
}
}
catch (IOException e)
{
throw getManifestUptodateCheckException(e);
}
return true;
}
private static MojoExecutionException getManifestUptodateCheckException(IOException e)
{
return new MojoExecutionException("Error checking manifest uptodate status", e);
}
private String getIncrementalData() {
return getInstructions().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("\n", "", "\n"));
}
private Path getIncrementalDataPath(MavenProject project) {
return Paths.get(project.getBuild().getDirectory(), "maven-bundle-plugin",
"org.apache.felix_maven-bundle-plugin_manifest_xx");
}
private long lastmod(Path p) {
try {
return lastModified(p);
} catch (IOException e) {
return 0;
}
}
private static long lastModified(Path p) throws IOException
{
return Files.getLastModifiedTime(p).toMillis();
}
private Stream<String> newer(long lastmod, File file) {
try {
if (file.isDirectory()) {
return Files.walk(file.toPath())
.filter(Files::isRegularFile)
.filter(p -> lastmod(p) > lastmod)
.map(Path::toString);
} else if (file.isFile()) {
if (lastmod(file.toPath()) > lastmod) {
if (file.getName().endsWith(".jar")) {
try (ZipFile zf = new ZipFile(file)) {
return zf.stream()
.filter(ze -> !ze.isDirectory())
.filter(ze -> ze.getLastModifiedTime().toMillis() > lastmod)
.map(ze -> file.toString() + "!" + ze.getName())
.collect(Collectors.toList())
.stream();
}
} else {
return Stream.of(file.toString());
}
} else {
return Stream.empty();
}
} else {
return Stream.empty();
}
} catch (IOException e) {
throw new IOError(e);
}
}
public static void writeManifest( Analyzer analyzer, File outputFile, boolean niceManifest,
boolean exportScr, File scrLocation, BuildContext buildContext, Log log ) throws Exception
{
Properties properties = analyzer.getProperties();
Jar jar = analyzer.getJar();
Manifest manifest = jar.getManifest();
if ( outputFile.exists() && properties.containsKey( "Merge-Headers" ) )
{
Manifest analyzerManifest = manifest;
manifest = new Manifest();
try( InputStream inputStream = new FileInputStream( outputFile ) )
{
manifest.read( inputStream );
}
Instructions instructions = new Instructions( ExtList.from( analyzer.getProperty("Merge-Headers") ) );
mergeManifest( instructions, manifest, analyzerManifest );
}
else
{
File parentFile = outputFile.getParentFile();
parentFile.mkdirs();
}
writeManifest( manifest, outputFile, niceManifest, buildContext, log );
if (exportScr)
{
exportScr(analyzer, jar, scrLocation, buildContext, log);
}
}
public static void writeManifest( Manifest manifest, File outputFile, boolean niceManifest,
BuildContext buildContext, Log log ) throws IOException
{
log.info("Writing manifest: " + outputFile.getPath());
outputFile.getParentFile().mkdirs();
try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() )
{
ManifestWriter.outputManifest( manifest, baos, niceManifest );
baos.flush();
byte[] newdata = baos.toByteArray();
byte[] curdata = new byte[0];
if (outputFile.exists())
{
curdata = Files.readAllBytes( outputFile.toPath() );
}
if ( !Arrays.equals( newdata, curdata ) )
{
try ( OutputStream os = buildContext.newFileOutputStream( outputFile ) )
{
os.write( newdata );
}
}
}
}
/*
* Patched version of bnd's Analyzer.calculateExportsFromContents
*/
public static String calculateExportsFromContents( Jar bundle )
{
String ddel = "";
StringBuffer sb = new StringBuffer();
Map<String, Map<String, Resource>> map = bundle.getDirectories();
for ( Iterator<Entry<String, Map<String, Resource>>> i = map.entrySet().iterator(); i.hasNext(); )
{
//----------------------------------------------------
// should also ignore directories with no resources
//----------------------------------------------------
Entry<String, Map<String, Resource>> entry = i.next();
if ( entry.getValue() == null || entry.getValue().isEmpty() )
continue;
//----------------------------------------------------
String directory = entry.getKey();
if ( directory.equals( "META-INF" ) || directory.startsWith( "META-INF/" ) )
continue;
if ( directory.equals( "OSGI-OPT" ) || directory.startsWith( "OSGI-OPT/" ) )
continue;
if ( directory.equals( "/" ) )
continue;
if ( directory.endsWith( "/" ) )
directory = directory.substring( 0, directory.length() - 1 );
directory = directory.replace( '/', '.' );
sb.append( ddel );
sb.append( directory );
ddel = ",";
}
return sb.toString();
}
}