blob: fdb0ce9cae52198ffa03384fa670849999bba2e6 [file] [log] [blame]
package org.apache.netbeans.nbm;
/*
* 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.
*/
import com.google.common.collect.Sets;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.AbstractArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolver;
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.apache.netbeans.nbm.utils.ExamineManifest;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.filters.StringInputStream;
import org.apache.tools.ant.taskdefs.Chmod;
import org.apache.tools.ant.types.FileSet;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.io.InputStreamFacade;
import org.netbeans.nbbuild.MakeListOfNBM;
/**
* Create the NetBeans module clusters/application for the 'nbm-application' packaging projects
*
* @author Milos Kleint
*/
@Mojo( name = "cluster-app",
defaultPhase = LifecyclePhase.PACKAGE,
requiresProject = true,
threadSafe = true,
requiresDependencyResolution = ResolutionScope.RUNTIME )
public class CreateClusterAppMojo
extends AbstractNbmMojo
{
/**
* output directory where the the NetBeans application will be created.
*/
@Parameter( defaultValue = "${project.build.directory}", required = true )
private File outputDirectory;
/**
* The Maven Project.
*/
@Parameter( required = true, readonly = true, property = "project" )
private MavenProject project;
/**
* The branding token for the application based on NetBeans platform.
*/
@Parameter( property = "netbeans.branding.token", required = true )
protected String brandingToken;
/**
* Optional path to custom etc/${brandingToken}.conf file. If not defined, a default template will be used.
*/
@Parameter( property = "netbeans.conf.file" )
private File etcConfFile;
/**
* Optional path to custom etc/${brandingToken}.clusters file. If not defined, a default one will be generated.
*/
@Parameter( property = "netbeans.clusters.file" )
private File etcClustersFile;
/**
* Directory which contains the executables that will be copied to the final application's bin/ directory. Please
* note that the name of the executables shall generally match the brandingToken parameter. Otherwise the
* application can be wrongly branded.
*/
@Parameter( property = "netbeans.bin.directory" )
private File binDirectory;
/**
* If the depending NBM file doesn't contain any application cluster information, use this value as default location
* for such module NBMs.
*
* @since 3.2
*/
@Parameter( defaultValue = "extra" )
private String defaultCluster;
/**
* attempts to verify the integrity of module artifacts making sure that all dependencies are included and that all
* required tokens are provided
*
* @since 3.10
*/
@Parameter( defaultValue = "true", property = "netbeans.verify.integrity" )
private boolean verifyIntegrity;
/**
* @since 4.12
*/
@Parameter( defaultValue = "org.netbeans", property = "groupIdPrefix" )
private String groupIdPrefix;
private final Collection<String> defaultPlatformTokens = Arrays.asList( new String[]
{
"org.openide.modules.os.Windows",
"org.openide.modules.os.Unix",
"org.openide.modules.os.MacOSX",
"org.openide.modules.os.OS2",
"org.openide.modules.os.PlainUnix",
"org.openide.modules.os.Linux",
"org.openide.modules.os.Solaris",
"org.openide.modules.ModuleFormat1",
"org.openide.modules.ModuleFormat2",
"org.openide.modules.jre.JavaFX" //MNBMODULE-234
} );
// <editor-fold defaultstate="collapsed" desc="Component parameters">
@Component
private ArtifactFactory artifactFactory;
@Component
private ArtifactResolver artifactResolver;
/**
* Local maven repository.
*
*/
@Parameter( required = true, readonly = true, property = "localRepository" )
protected ArtifactRepository localRepository;
// end of component params custom code folding
// </editor-fold>
@Override
public void execute()
throws MojoExecutionException, MojoFailureException
{
File nbmBuildDirFile = new File( outputDirectory, brandingToken );
if ( !nbmBuildDirFile.exists() )
{
nbmBuildDirFile.mkdirs();
}
if ( "nbm-application".equals( project.getPackaging() ) )
{
Project antProject = registerNbmAntTasks();
Set<String> wrappedBundleCNBs = new HashSet<>( 100 );
Map<String, Set<String>> clusterDependencies = new HashMap<>();
Map<String, Set<String>> clusterModules = new HashMap<>();
//verify integrity
Set<String> modulesCNBs = new HashSet<>( SET_INITIAL_SIZE );
Set<String> dependencyCNBs = new HashSet<>( SET_INITIAL_SIZE );
Map<String, Set<String>> dependencyCNBBacktraces = new HashMap<>( MAP_INITIALSIZE );
Set<String> requireTokens = new HashSet<>( MAP_INITIALSIZE );
Map<String, Set<String>> requireTokensBacktraces = new HashMap<>( MAP_INITIALSIZE );
Set<String> provideTokens = new HashSet<>( MAP_INITIALSIZE );
Set<String> osgiImports = new HashSet<>( MAP_INITIALSIZE );
Map<String, Set<String>> osgiImportsBacktraces = new HashMap<>( MAP_INITIALSIZE );
Set<String> osgiExports = new HashSet<>( MAP_INITIALSIZE );
//a way to deal with nb module declaring xxx.** (subpackages) declaration that is consumed by osgi imports
Set<String> osgiExportsSubs = new HashSet<>( MAP_INITIALSIZE );
List<BundleTuple> bundles = new ArrayList<>();
@SuppressWarnings( "unchecked" )
Set<Artifact> artifacts = project.getArtifacts();
for ( Artifact art : artifacts )
{
ArtifactResult res
= turnJarToNbmFile( art, artifactFactory, artifactResolver, project,
localRepository );
if ( res.hasConvertedArtifact() )
{
art = res.getConvertedArtifact();
}
if ( art.getType().equals( "nbm-file" ) )
{
try
{
JarFile jf = new JarFile( art.getFile() );
try
{
String clusterName = findCluster( jf );
ClusterTuple cluster = processCluster( clusterName, nbmBuildDirFile, art );
getLog().debug( "Copying " + art.getId() + " to cluster " + clusterName );
Enumeration<JarEntry> enu = jf.entries();
// we need to trigger this ant task to generate the update_tracking file.
MakeListOfNBM makeTask = (MakeListOfNBM) antProject.createTask( "genlist" );
antProject.setNewProperty( "module.name", art.getFile().getName() ); // TODO
antProject.setProperty( "cluster.dir", clusterName );
FileSet set = makeTask.createFileSet();
set.setDir( cluster.location );
makeTask.setOutputfiledir( cluster.location );
String[] executables = null;
File classpathRoot = null;
String classPath = null;
while ( enu.hasMoreElements() )
{
JarEntry ent = enu.nextElement();
String name = ent.getName();
//MNBMODULE-176
if ( name.equals( "Info/executables.list" ) )
{
if ( cluster.newer )
{
InputStream is = jf.getInputStream( ent );
executables = StringUtils.split( IOUtil.toString( is, "UTF-8" ), "\n" );
}
}
else if ( name.startsWith( "netbeans/" ) )
{ // ignore everything else.
String path = clusterName + name.substring( "netbeans".length() );
boolean ispack200 = path.endsWith( ".jar.pack.gz" );
if ( ispack200 )
{
path = path.replace( ".jar.pack.gz", ".jar" );
}
File fl = new File( nbmBuildDirFile, path.replace( "/", File.separator ) );
String part = name.substring( "netbeans/".length() );
if ( ispack200 )
{
part = part.replace( ".jar.pack.gz", ".jar" );
}
if ( cluster.newer )
{
if ( ent.isDirectory() )
{
fl.mkdirs();
}
else if ( path.endsWith( ".external" ) ) // MNBMODULE-138
{
InputStream is = jf.getInputStream( ent );
try
{
externalDownload( new File( fl.getParentFile(),
fl.getName().replaceFirst( "[.]external$",
"" ) ), is );
}
finally
{
is.close();
}
//MNBMODULE-192
set.appendIncludes( new String[]
{
name.substring( "netbeans/".length(), name.length() - ".external".
length() )
} );
}
else
{
set.appendIncludes( new String[]
{
part
} );
fl.getParentFile().mkdirs();
fl.createNewFile();
BufferedOutputStream outstream = null;
try
{
outstream = new BufferedOutputStream( new FileOutputStream( fl ) );
InputStream instream = jf.getInputStream( ent );
if ( ispack200 )
{
try ( JarOutputStream jos = new JarOutputStream( outstream ) )
{
Pack200.Unpacker unp = Pack200.newUnpacker();
GZIPInputStream gzip = new GZIPInputStream( instream );
unp.unpack( gzip, jos );
}
catch ( LinkageError cnfe )
{
throw new BuildException( "Using jdk 14 and later prevents "
+ "reading of NBM created with pack200" );
}
}
else
{
IOUtil.copy( instream, outstream );
}
}
finally
{
IOUtil.close( outstream );
}
}
}
// TODO examine netbeans/config/Modules to see if the module is autoload/eager
// in verifyIntegrity these could be handled more gracefully than regular modules.
// eager is simpler, does not need to have module dependencies satisfied.
// autoload needs checking if any of the other modules declares a dependency on it.
// if not, also safe to ignore?
// now figure which one of the jars is the module jar..
if ( part.matches( "(modules|core|lib)/[^/]+[.]jar" ) )
{
ExamineManifest ex = new ExamineManifest( getLog() );
ex.setJarFile( fl );
ex.setPopulateDependencies( true );
ex.checkFile();
if ( ex.isNetBeansModule() )
{
makeTask.setModule( part );
addToMap( clusterDependencies, clusterName, ex.getDependencyTokens() );
addToMap( clusterModules, clusterName, Collections.singletonList( ex.
getModule() ) );
if ( ex.getClasspath().length() > 0 )
{ //MNBMODULE-220
try
{
classPath = URLDecoder.decode(ex.getClasspath(), "UTF-8");
} catch (UnsupportedEncodingException exception)
{
throw new IllegalStateException(exception);
}
classpathRoot = fl.getParentFile();
}
}
if ( verifyIntegrity )
{
dependencyCNBs.addAll( ex.getDependencyTokens() );
modulesCNBs.add( ex.getModule() );
for ( String d : ex.getDependencyTokens() )
{
addToMap( dependencyCNBBacktraces, d, Collections.singletonList( ex.
getModule() ) );
}
if ( ex.isNetBeansModule() )
{
requireTokens.addAll( ex.getNetBeansRequiresTokens() );
for ( String r : ex.getNetBeansRequiresTokens() )
{
addToMap( requireTokensBacktraces, r, Collections.singletonList( ex.
getModule() ) );
}
provideTokens.addAll( ex.getNetBeansProvidesTokens() );
for ( String pack : ex.getPackages() )
{
if ( pack.endsWith( ".**" ) )
{
//what to do with subpackages?
pack = pack.substring( 0, pack.length() - ".**".length() );
osgiExportsSubs.add( pack );
}
else if ( pack.endsWith( ".*" ) )
{
pack = pack.substring( 0, pack.length() - ".*".length() );
osgiExports.add( pack );
}
}
}
}
}
}
}
if ( classPath != null )
{ // MNBMODULE-220 collect wrappedbundleCNBs, later useful in assignClustersToBundles(),
// these get removed from list of bundles.
String[] paths = StringUtils.split( classPath, " " );
for ( String path : paths )
{
path = path.trim();
File classpathFile = new File( classpathRoot, path );
if ( path.equals( "${java.home}/lib/ext/jfxrt.jar" ) )
{ //MNBMODULE-228
String jhm = System.getProperty( "java.home" );
classpathFile = new File( new File( new File( new File( jhm ), "lib" ), "ext" ),
"jfxrt.jar" );
if ( !classpathFile.exists() )
{
File jdk7 = new File( new File( new File( jhm ), "lib" ), "jfxrt.jar" );
if ( jdk7.exists() )
{
classpathFile = jdk7;
}
}
}
if ( !classpathFile.isFile() )
{
getLog().warn( "Could not resolve Class-Path item in " + art.getId()
+ ", path is:" + path + ", skipping" );
continue; //try to guard against future failures
}
ExamineManifest ex = new ExamineManifest( getLog() );
ex.setJarFile( classpathFile );
//ex.setPopulateDependencies( true );
ex.checkFile();
if ( ex.isOsgiBundle() )
{
if ( art.getId().contains( groupIdPrefix
+ ".modules:org-netbeans-modules-maven-embedder" ) )
{
// in this case we dont want module-maven-embedder to be considered as
// wrapper for his libs guava is provided but ide have it also
}
else
{
getLog().info( ex.getModule() + " added by " + art.getId() + ""
+ classpathFile );
wrappedBundleCNBs.add( ex.getModule() );
}
}
}
}
if ( cluster.newer )
{
try
{
makeTask.execute();
}
catch ( BuildException e )
{
getLog().error( "Cannot Generate update_tracking XML file from " + art.getFile() );
throw new MojoExecutionException( e.getMessage(), e );
}
if ( executables != null )
{
//MNBMODULE-176
for ( String exec : executables )
{
exec = exec.replace( "/", File.separator );
File execFile = new File( cluster.location, exec );
if ( execFile.exists() )
{
execFile.setExecutable( true, false );
}
}
}
}
}
finally
{
jf.close();
}
}
catch ( IOException ex )
{
getLog().error( art.getFile().getAbsolutePath(), ex );
}
}
if ( res.isOSGiBundle() )
{
ExamineManifest ex = res.getExaminedManifest();
bundles.add( new BundleTuple( art, ex ) );
if ( verifyIntegrity )
{
dependencyCNBs.addAll( ex.getDependencyTokens() );
for ( String d : ex.getDependencyTokens() )
{
addToMap( dependencyCNBBacktraces, d, Collections.singletonList( ex.getModule() ) );
}
modulesCNBs.add( ex.getModule() );
osgiImports.addAll( ex.getOsgiImports() );
for ( String d : ex.getOsgiImports() )
{
addToMap( osgiImportsBacktraces, d, Collections.singletonList( ex.getModule() ) );
}
osgiExports.addAll( ex.getOsgiExports() );
}
}
}
if ( verifyIntegrity )
{
if ( getLog().isDebugEnabled() )
{
getLog().debug( "All found codenamebases:" + Arrays.toString( modulesCNBs.toArray() ) );
getLog().debug( "All found OSGI exports:" + Arrays.toString( osgiExports.toArray() ) );
getLog().debug( "All found provided tokens:" + Arrays.toString( provideTokens.toArray() ) );
}
dependencyCNBs.removeAll( modulesCNBs );
if ( modulesCNBs.contains( "org.netbeans.modules.netbinox" ) )
{
dependencyCNBs.remove( "org.eclipse.osgi" ); //this is special.
}
osgiImports.removeAll( osgiExports );
Iterator<String> it = osgiImports.iterator();
while ( it.hasNext() )
{
String s = it.next();
if ( s.startsWith( "java." )
|| s.startsWith( "javax." )
|| s.startsWith( "sun." )
|| s.startsWith( "org.xml.sax" )
|| s.startsWith( "org.w3c.dom" )
|| s.startsWith( "org.ietf.jgss" ) )
{
it.remove();
continue;
}
for ( String sub : osgiExportsSubs )
{
if ( s.startsWith( sub ) )
{
it.remove();
break;
}
}
}
requireTokens.removeAll( provideTokens );
requireTokens.removeAll( defaultPlatformTokens );
if ( !dependencyCNBs.isEmpty() || !osgiImports.isEmpty() || !requireTokens.isEmpty() )
{
if ( !dependencyCNBs.isEmpty() )
{
getLog().error(
"Some included modules/bundles depend on these codenamebases but they are not included."
+ " The application will fail starting up. The missing codenamebases are:" );
for ( String s : dependencyCNBs )
{
Set<String> back = dependencyCNBBacktraces.get( s );
getLog().error( " " + s + ( back != null ? " ref: " + Arrays.toString( back.
toArray() ) : "" ) );
}
}
if ( !osgiImports.isEmpty() )
{
getLog().error(
"Some OSGi imports are not satisfied by included bundles' exports. "
+ "The application will fail starting up. The missing imports are:" );
for ( String s : osgiImports )
{
Set<String> back = osgiImportsBacktraces.get( s );
getLog().error( " " + s + ( back != null ? " ref: " + Arrays.toString( back.
toArray() ) : "" ) );
}
}
if ( !requireTokens.isEmpty() )
{
getLog().error(
"Some tokens required by included modules are not provided by included modules. "
+ "The application will fail starting up. The missing tokens are:" );
for ( String s : requireTokens )
{
Set<String> back = requireTokensBacktraces.get( s );
getLog().error( " " + s + ( back != null ? " ref: " + Arrays.toString( back.
toArray() ) : "" ) );
}
}
throw new MojoFailureException(
"See above for consistency validation check failures. "
+ " Either fix those by adding the relevant dependencies to the application or "
+ "disable the check by setting the verifyIntegrity parameter to false or by running with "
+ "-Dnetbeans.verify.integrity=false cmd line parameter." );
}
else
{
getLog().info( "Integrity verification passed." );
}
}
else
{
getLog().info( "Integrity verification skipped." );
}
//attempt to sort clusters based on the dependencies and cluster content.
Map<String, Set<String>> cluster2depClusters =
computeClusterOrdering( clusterDependencies, clusterModules );
clusterModules.clear();
//now assign the cluster to bundles based on dependencies..
assignClustersToBundles( bundles, wrappedBundleCNBs, clusterDependencies, cluster2depClusters, getLog() );
for ( BundleTuple ent : bundles )
{
Artifact art = ent.artifact;
final ExamineManifest ex = ent.manifest;
String clstr = ent.cluster;
if ( clstr == null )
{
clstr = defaultCluster;
}
ClusterTuple cluster = processCluster( clstr, nbmBuildDirFile, art );
if ( cluster.newer )
{
getLog().info( "Copying " + art.getId() + " to cluster " + clstr );
File modules = new File( cluster.location, "modules" );
modules.mkdirs();
File config = new File( cluster.location, "config" );
File confModules = new File( config, "Modules" );
confModules.mkdirs();
File updateTracking = new File( cluster.location, "update_tracking" );
updateTracking.mkdirs();
final String cnb = ex.getModule();
final String cnbDashed = cnb.replace( ".", "-" );
//do we need the file in some canotical name pattern in moduleArt?
final File moduleArt = new File( modules, cnbDashed + ".jar" );
final String specVer = ex.getSpecVersion();
try
{
FileUtils.copyFile( art.getFile(), moduleArt );
final File moduleConf = new File( confModules, cnbDashed + ".xml" );
FileUtils.copyStreamToFile( new InputStreamFacade()
{
@Override
public InputStream getInputStream() throws IOException
{
return new StringInputStream( createBundleConfigFile( cnb, ex.isBundleAutoload() ),
"UTF-8" );
}
}, moduleConf );
FileUtils.copyStreamToFile( new InputStreamFacade()
{
@Override
public InputStream getInputStream() throws IOException
{
return new StringInputStream( createBundleUpdateTracking( cnb, moduleArt, moduleConf,
specVer ), "UTF-8" );
}
}, new File( updateTracking, cnbDashed + ".xml" ) );
}
catch ( IOException exc )
{
getLog().error( exc );
}
}
}
getLog().info(
"Created NetBeans module cluster(s) at " + nbmBuildDirFile.getAbsoluteFile() );
}
else
{
throw new MojoExecutionException(
"This goal only makes sense on project with nbm-application packaging" );
}
//in 6.1 the rebuilt modules will be cached if the timestamp is not touched.
File[] files = nbmBuildDirFile.listFiles();
for ( File file : files )
{
if ( file.isDirectory() )
{
File stamp = new File( file, ".lastModified" );
if ( !stamp.exists() )
{
try
{
stamp.createNewFile();
}
catch ( IOException ex )
{
ex.printStackTrace();
}
}
stamp.setLastModified( new Date().getTime() );
}
}
try
{
createBinEtcDir( nbmBuildDirFile, brandingToken );
}
catch ( IOException ex )
{
throw new MojoExecutionException(
"Cannot process etc folder content creation.", ex );
}
}
private static final int SET_INITIAL_SIZE = 200;
private static final int MAP_INITIALSIZE = 50;
private static final Pattern PATT = Pattern.compile(
".*targetcluster=\"([a-zA-Z0-9_\\.\\-]+)\".*", Pattern.DOTALL );
private String findCluster( JarFile jf )
throws MojoFailureException, IOException
{
ZipEntry entry = jf.getEntry( "Info/info.xml" );
InputStream ins = jf.getInputStream( entry );
String str = IOUtil.toString( ins, "UTF8" );
Matcher m = PATT.matcher( str );
if ( !m.matches() )
{
getLog().info( "Cannot find cluster for " + jf.getName() + " Falling back to default value - '"
+ defaultCluster + "'." );
return defaultCluster;
}
else
{
return m.group( 1 );
}
}
/**
*
* @param buildDir Directory where the platform bundle is built
* @param brandingToken
*
* @throws java.io.IOException
*/
private void createBinEtcDir( File buildDir, String brandingToken )
throws IOException, MojoExecutionException
{
File etcDir = new File( buildDir + File.separator + "etc" );
etcDir.mkdir();
// create app.clusters which contains a list of clusters to include in the application
File clusterConf = new File( etcDir + File.separator + brandingToken + ".clusters" );
String clustersString;
if ( etcClustersFile != null )
{
clustersString = FileUtils.fileRead( etcClustersFile, "UTF-8" );
}
else
{
clusterConf.createNewFile();
StringBuilder buffer = new StringBuilder();
File[] clusters = buildDir.listFiles( new FileFilter()
{
@Override
public boolean accept( File pathname )
{
return new File( pathname, ".lastModified" ).exists();
}
} );
for ( File cluster : clusters )
{
buffer.append( cluster.getName() );
buffer.append( "\n" );
}
clustersString = buffer.toString();
}
FileUtils.fileWrite( clusterConf.getAbsolutePath(), clustersString );
File confFile = etcConfFile;
String str;
if ( confFile == null )
{
File harnessDir = new File( buildDir, "harness" );
// app.conf contains default options and other settings
confFile = new File(
harnessDir.getAbsolutePath() + File.separator + "etc" + File.separator + "app.conf" );
if ( confFile.exists() )
{
str = FileUtils.fileRead( confFile, "UTF-8" );
}
else
{
getLog().debug( "Using fallback app.conf shipping with the nbm-maven-plugin." );
InputStream instream = null;
try
{
instream = getClass().getClassLoader().getResourceAsStream( "harness/etc/app.conf" );
str = IOUtil.toString( instream, "UTF-8" );
}
finally
{
IOUtil.close( instream );
}
}
}
else
{
str = FileUtils.fileRead( confFile, "UTF-8" );
}
File confDestFile = new File(
etcDir.getAbsolutePath() + File.separator + brandingToken + ".conf" );
str = str.replace( "${branding.token}", brandingToken );
FileUtils.fileWrite( confDestFile.getAbsolutePath(), "UTF-8", str );
File destBinDir = new File( buildDir + File.separator + "bin" );
destBinDir.mkdir();
File binDir;
File destExeW = new File( destBinDir, brandingToken + "_w.exe" );
File destExe = new File( destBinDir, brandingToken + ".exe" );
File destExe64 = new File( destBinDir, brandingToken + "64.exe" );
File destSh = new File( destBinDir, brandingToken );
File harnessDir = new File( buildDir, "harness" );
//we have org-netbeans-modules-apisupport-harness in target area, just use it's own launchers.
binDir = new File(
harnessDir.getAbsolutePath() + File.separator + "launchers" );
if ( binDir.exists() )
{
File exe = new File( binDir, "app.exe" );
FileUtils.copyFile( exe, destExe );
File exe64 = new File( binDir, "app64.exe" );
if ( exe64.isFile() )
{
FileUtils.copyFile( exe64, destExe64 );
}
File exew = new File( binDir, "app_w.exe" );
if ( exew.exists() ) //in 6.7 the _w.exe file is no more.
{
FileUtils.copyFile( exew, destExeW );
}
File sh = new File( binDir, "app.sh" );
FileUtils.copyFile( sh, destSh );
}
else
{
File nbm = getHarnessNbm();
try ( ZipFile zip = new ZipFile( nbm ) )
{
getLog().debug(
"Using fallback executables from downloaded org-netbeans-modules-apisupport-harness nbm file." );
writeFromZip( zip, "netbeans/launchers/app.sh", destSh, true );
writeFromZip( zip, "netbeans/launchers/app.exe", destExe, true );
writeFromZip( zip, "netbeans/launchers/app64.exe", destExe64, false );
writeFromZip( zip, "netbeans/launchers/app_w.exe", destExeW, false );
}
}
if ( binDirectory != null )
{
//we have custom launchers, only overwrite the ones the user provided.
binDir = binDirectory;
File[] fls = binDir.listFiles();
if ( fls == null )
{
throw new MojoExecutionException( "Parameter 'binDirectory' has to point to an existing folder." );
}
for ( File fl : fls )
{
String name = fl.getName();
File dest = null;
if ( name.endsWith( "_w.exe" ) )
{
dest = destExeW;
}
else if ( name.endsWith( "64.exe" ) )
{
dest = destExe64;
}
else if ( name.endsWith( ".exe" ) )
{
dest = destExe;
}
else if ( !name.contains( "." ) || name.endsWith( ".sh" ) )
{
dest = destSh;
}
if ( dest != null && fl.exists() ) //in 6.7 the _w.exe file is no more.
{
FileUtils.copyFile( fl, dest );
}
else
{
//warn about file not being copied
}
}
}
Project antProject = antProject();
Chmod chmod = (Chmod) antProject.createTask( "chmod" );
FileSet fs = new FileSet();
fs.setDir( destBinDir );
fs.setIncludes( "*" );
chmod.addFileset( fs );
chmod.setPerm( "755" );
chmod.execute();
}
private void writeFile( String path, File destSh )
throws IOException
{
InputStream instream = null;
OutputStream output = null;
try
{
instream = getClass().getClassLoader().getResourceAsStream( path );
if ( instream == null )
{
throw new FileNotFoundException( path );
}
destSh.createNewFile();
output = new BufferedOutputStream( new FileOutputStream( destSh ) );
IOUtil.copy( instream, output );
}
finally
{
IOUtil.close( instream );
IOUtil.close( output );
}
}
private ClusterTuple processCluster( String cluster, File nbmBuildDirFile, Artifact art )
{
File clusterFile = new File( nbmBuildDirFile, cluster );
boolean newer = false;
if ( !clusterFile.exists() )
{
clusterFile.mkdir();
newer = true;
}
else
{
File stamp = new File( clusterFile, ".lastModified" );
if ( stamp.lastModified() < art.getFile().lastModified() )
{
newer = true;
}
}
return new ClusterTuple( clusterFile, newer );
}
private void externalDownload( File f, InputStream is )
throws IOException
{
// Cf. org.netbeans.nbbuild.AutoUpdate
BufferedReader r = new BufferedReader( new InputStreamReader( is, "UTF-8" ) );
long crc = -1;
long size = -1;
boolean found = false;
String line;
while ( ( line = r.readLine() ) != null )
{
if ( line.startsWith( "CRC:" ) )
{
crc = Long.parseLong( line.substring( 4 ).trim() );
}
else if ( line.startsWith( "URL:" ) )
{
String rest = line.substring( 4 ).trim();
if ( rest.startsWith( "m2:/" ) )
{
if ( !found )
{
String[] coords = rest.substring( 4 ).trim().split( ":" );
Artifact artifact;
if ( coords.length == 4 )
{
artifact = artifactFactory.
createArtifact( coords[0], coords[1], coords[2], null, coords[3] );
}
else
{
artifact = artifactFactory.createArtifactWithClassifier( coords[0], coords[1], coords[2],
coords[3], coords[4] );
}
try
{
artifactResolver.
resolve( artifact, project.getRemoteArtifactRepositories(), localRepository );
FileUtils.copyFile( artifact.getFile(), f );
found = true;
}
catch ( AbstractArtifactResolutionException x )
{
getLog().warn( "Cannot find " + line.substring( 8 ), x );
}
}
}
else if ( !found )
{
String url = line.substring( 4 ).trim();
try
{
// XXX use Wagon API instead
FileUtils.copyURLToFile( new URL( url ), f );
found = true;
}
catch ( IOException x )
{
getLog().warn( "Cannot download " + url, x );
}
}
}
else if ( line.startsWith( "SIZE:" ) )
{
size = Long.parseLong( line.substring( 5 ).trim() );
}
else
{
getLog().warn( "Unrecognized line: " + line );
}
}
if ( !found )
{
throw new IOException( "Could not download " + f );
}
if ( crc != -1 && crc != crcForFile( f ).getValue() )
{
throw new IOException( "CRC-32 of " + f + " does not match declared " + crc );
}
if ( size != -1 && size != f.length() )
{
throw new IOException( "Size of " + f + " does not match declared " + size );
}
}
private File getHarnessNbm() throws MojoExecutionException
{
@SuppressWarnings( "unchecked" )
Set<Artifact> artifacts = project.getArtifacts();
String version = null;
for ( Artifact a : artifacts )
{
if ( ( groupIdPrefix + ".modules" ).equals( a.getGroupId() ) && "org-netbeans-bootstrap".equals( a.
getArtifactId() ) )
{
//base version in non-snapshot should equals version, in snapshots to X-SNAPSHOT, not timestamp
version = a.getBaseVersion();
break;
}
}
if ( version == null )
{
throw new MojoExecutionException(
"We could not find org-netbeans-bootstrap among the modules in the application. "
+ "Launchers could not be found." );
}
Artifact nbmArt = artifactFactory.createArtifact(
groupIdPrefix + ".modules",
"org-netbeans-modules-apisupport-harness",
version,
"compile",
"nbm-file" );
try
{
artifactResolver.resolve( nbmArt, project.getRemoteArtifactRepositories(), localRepository );
}
catch ( ArtifactResolutionException | ArtifactNotFoundException ex )
{
throw new MojoExecutionException( "Failed to retrieve the nbm file from repository", ex );
}
return nbmArt.getFile();
}
private void writeFromZip( final ZipFile zip, String zipPath, File destFile, boolean mandatory ) throws
MojoExecutionException, IOException
{
final ZipEntry path = zip.getEntry( zipPath );
if ( path == null )
{
if ( mandatory )
{
throw new MojoExecutionException( zipPath + " not found in " + zip.getName() );
}
getLog().debug( zipPath + " is not present in " + zip.getName() );
return;
}
FileUtils.copyStreamToFile( new InputStreamFacade()
{
@Override
public InputStream getInputStream() throws IOException
{
return zip.getInputStream( path );
}
}, destFile );
}
private static void addToMap( Map<String, Set<String>> map, String clusterName, List<String> newValues )
{
Set<String> lst = map.get( clusterName );
if ( lst == null )
{
lst = new HashSet<>();
map.put( clusterName, lst );
}
if ( newValues != null )
{
lst.addAll( newValues );
}
}
private static List<String> findByDependencies( Map<String, Set<String>> clusterDependencies, String spec )
{
List<String> toRet = new ArrayList<>();
for ( Map.Entry<String, Set<String>> entry : clusterDependencies.entrySet() )
{
if ( entry.getValue().contains( spec ) )
{
toRet.add( entry.getKey() );
}
}
return toRet;
}
//the basic idea is that bundle's cluster can be determined by who depends on it.
//simplest case is when a module depends on it. If there are more, we need to pick one that is "lower in the stack,
//that's what cluster2depClusters is for.
//the rest needs to be determined in more sofisticated manner.
//start from bundles with known cluster and see what other bundles they depend on.
//stamp all these with the same cluster. do it recursively.
//At the end process the remaining bundles in reverse order.
//Check if *they* depend on a bundle with known cluster and so on..
//A few unsolved cases:
// - we never update the cluster information once a match was found,
// but there is a possibility that later in the processing the cluster could be "lowered".
// - 2 or more modules from unrelated clusters we cannot easily decide,
// most likely should be in common denominator cluster but our cluster2depClusters map is not transitive,
// only lists direct dependencies
static void assignClustersToBundles( List<BundleTuple> bundles, Set<String> wrappedBundleCNBs,
Map<String, Set<String>> clusterDependencies,
Map<String, Set<String>> cluster2depClusters, Log log )
{
List<BundleTuple> toProcess = new ArrayList<>();
List<BundleTuple> known = new ArrayList<>();
for ( Iterator<BundleTuple> it = bundles.iterator(); it.hasNext(); )
{
BundleTuple ent = it.next();
Artifact art = ent.artifact;
ExamineManifest ex = ent.manifest;
String spec = ex.getModule();
//null check for tests
//have a way to force inclusion of osgi items. Direct dependency is never wrapped by modules.
if ( art != null && art.getDependencyTrail().size() > 2 && wrappedBundleCNBs.contains( spec ) )
{
// we already have this one as a wrapped module.
log.debug( "Not including bundle " + art.getDependencyConflictId()
+ ". It is already included in a NetBeans module" );
it.remove();
continue;
}
List<String> depclusters = findByDependencies( clusterDependencies, spec );
if ( depclusters.size() == 1 )
{
ent.cluster = depclusters.get( 0 );
known.add( ent );
}
else if ( depclusters.isEmpty() )
{
toProcess.add( ent );
}
else
{
//more results.. from 2 dependent clusters pick the one that is lower in the stack.
for ( Iterator<String> it2 = depclusters.iterator(); it2.hasNext(); )
{
String s = it2.next();
Set<String> depsCs = cluster2depClusters.get( s );
boolean removeS = false;
for ( String sDep : depclusters )
{
if ( s.equals( sDep ) )
{
continue;
}
if ( depsCs != null && depsCs.contains( sDep ) )
{
removeS = true;
}
}
if ( removeS )
{
it2.remove();
}
}
//TODO still some free room there,
//what if they don't directly depend on each other but still are related
ent.cluster = depclusters.get( 0 );
known.add( ent );
}
}
if ( !toProcess.isEmpty() )
{
walkKnownBundleDependenciesDown( known, toProcess );
}
if ( !toProcess.isEmpty() )
{
walkKnownBundleDependenciesUp( known, toProcess );
}
}
private static void walkKnownBundleDependenciesDown( List<BundleTuple> known, List<BundleTuple> toProcess )
{
boolean atLeastOneWasFound = false;
for ( Iterator<BundleTuple> it = toProcess.iterator(); it.hasNext(); )
{
BundleTuple bundleTuple = it.next();
boolean found = false;
for ( BundleTuple knownBT : known )
{
Sets.SetView<String> is = Sets.intersection( bundleTuple.manifest.getOsgiExports(), knownBT.manifest.
getOsgiImports() );
if ( !is.isEmpty() )
{
found = true;
bundleTuple.cluster = knownBT.cluster;
break;
}
//dependencyTokens are requireBundle - matches the module property
is = Sets.intersection( Collections.singleton( bundleTuple.manifest.getModule() ), new HashSet(
knownBT.manifest.getDependencyTokens() ) );
if ( !is.isEmpty() )
{
found = true;
bundleTuple.cluster = knownBT.cluster;
break;
}
}
if ( found )
{
atLeastOneWasFound = true;
it.remove();
known.add( bundleTuple );
}
}
if ( !toProcess.isEmpty() && atLeastOneWasFound )
{
walkKnownBundleDependenciesDown( known, toProcess );
}
}
private static void walkKnownBundleDependenciesUp( List<BundleTuple> known, List<BundleTuple> toProcess )
{
boolean atLeastOneWasFound = false;
for ( Iterator<BundleTuple> it = toProcess.iterator(); it.hasNext(); )
{
BundleTuple bundleTuple = it.next();
boolean found = false;
for ( BundleTuple knownBT : known )
{
Sets.SetView<String> is = Sets.intersection( bundleTuple.manifest.getOsgiImports(), knownBT.manifest.
getOsgiExports() );
if ( !is.isEmpty() )
{
found = true;
bundleTuple.cluster = knownBT.cluster;
break;
}
//dependencyTokens are requireBundle - matches the module property
is = Sets.intersection( Collections.singleton( knownBT.manifest.getModule() ), new HashSet(
bundleTuple.manifest.getDependencyTokens() ) );
if ( !is.isEmpty() )
{
found = true;
bundleTuple.cluster = knownBT.cluster;
break;
}
}
if ( found )
{
atLeastOneWasFound = true;
it.remove();
known.add( bundleTuple );
}
}
if ( !toProcess.isEmpty() && atLeastOneWasFound )
{
walkKnownBundleDependenciesDown( known, toProcess );
}
if ( !toProcess.isEmpty() && atLeastOneWasFound )
{
walkKnownBundleDependenciesUp( known, toProcess );
}
}
//static and default for tests..
static Map<String, Set<String>> computeClusterOrdering( Map<String, Set<String>> clusterDependencies,
Map<String, Set<String>> clusterModules )
{
Map<String, Set<String>> cluster2depClusters = new HashMap<>();
for ( Map.Entry<String, Set<String>> entry : clusterDependencies.entrySet() )
{
String cluster = entry.getKey();
Set<String> deps = entry.getValue();
for ( Map.Entry<String, Set<String>> subEnt : clusterModules.entrySet() )
{
if ( subEnt.getKey().equals( cluster ) )
{
continue;
}
Sets.SetView<String> is = Sets.intersection( subEnt.getValue(), deps );
if ( !is.isEmpty() )
{
addToMap( cluster2depClusters, cluster, Collections.singletonList( subEnt.getKey() ) );
}
}
}
return cluster2depClusters;
}
static class BundleTuple
{
final Artifact artifact;
final ExamineManifest manifest;
String cluster;
BundleTuple( Artifact artifact, ExamineManifest manifest )
{
this.artifact = artifact;
this.manifest = manifest;
}
}
private static class ClusterTuple
{
final File location;
final boolean newer;
private ClusterTuple( File clusterFile, boolean newer )
{
location = clusterFile;
this.newer = newer;
}
}
static String createBundleConfigFile( String cnb, boolean autoload )
{
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<!DOCTYPE module PUBLIC \"-//NetBeans//DTD Module Status 1.0//EN\"\n"
+ " \"http://www.netbeans.org/dtds/module-status-1_0.dtd\">\n"
+ "<module name=\"" + cnb + "\">\n"
+ " <param name=\"autoload\">" + autoload + "</param>\n"
+ " <param name=\"eager\">false</param>\n"
+ ( autoload ? "" : " <param name=\"enabled\">true</param>\n" )
+ " <param name=\"jar\">modules/" + cnb.replace( ".", "-" ) + ".jar</param>\n"
+ " <param name=\"reloadable\">false</param>\n"
+ "</module>\n";
}
static String createBundleUpdateTracking( String cnb, File moduleArt, File moduleConf, String specVersion )
throws FileNotFoundException, IOException
{
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<module codename=\"" + cnb + "\">\n"
+ " <module_version install_time=\"" + System.currentTimeMillis()
+ "\" last=\"true\" origin=\"installer\" specification_version=\"" + specVersion + "\">\n"
+ " <file crc=\"" + crcForFile( moduleConf ).getValue() + "\" name=\"config/Modules/" + cnb.
replace( ".", "-" ) + ".xml\"/>\n"
+ " <file crc=\"" + crcForFile( moduleArt ).getValue() + "\" name=\"modules/" + cnb.replace( ".",
"-" )
+ ".jar\"/>\n"
+ " </module_version>\n"
+ "</module>";
}
static CRC32 crcForFile( File inFile )
throws FileNotFoundException, IOException
{
CRC32 crc = new CRC32();
try ( InputStream inFileStream = new FileInputStream( inFile ) )
{
byte[] array = new byte[(int) inFile.length()];
int len = inFileStream.read( array );
if ( len != array.length )
{
throw new IOException( "Cannot fully read " + inFile );
}
crc.update( array );
}
return crc;
}
}