package org.codehaus.mojo.osxappbundle; /* * Copyright 2001-2008 The Codehaus. * * Licensed 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 org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout; import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; import org.apache.velocity.VelocityContext; import org.apache.velocity.exception.MethodInvocationException; import org.apache.velocity.exception.ParseErrorException; import org.apache.velocity.exception.ResourceNotFoundException; import org.codehaus.plexus.archiver.ArchiverException; import org.codehaus.plexus.archiver.zip.ZipArchiver; import org.codehaus.plexus.util.DirectoryScanner; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.cli.CommandLineException; import org.codehaus.plexus.util.cli.Commandline; import org.codehaus.plexus.velocity.VelocityComponent; import org.codehaus.mojo.osxappbundle.encoding.DefaultEncodingDetector; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.StringWriter; import java.io.ByteArrayInputStream; import java.io.Writer; import java.io.OutputStreamWriter; import java.io.FileOutputStream; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.Arrays; /** * Package dependencies as an Application Bundle for Mac OS X. * * @goal bundle * @phase package * @requiresDependencyResolution runtime */ public class CreateApplicationBundleMojo extends AbstractMojo { /** * Default includes - everything is included. */ private static final String[] DEFAULT_INCLUDES = {"**/**"}; /** * The Maven Project Object * * @parameter default-value="${project}" * @readonly */ private MavenProject project; /** * The directory where the application bundle will be created * * @parameter default-value="${project.build.directory}/${project.build.finalName}"; */ private File buildDirectory; /** * The location of the generated disk image file * * @parameter default-value="${project.build.directory}/${project.build.finalName}.dmg" */ private File diskImageFile; /** * The location of the Java Application Stub * * @parameter default-value="/System/Library/Frameworks/JavaVM.framework/Versions/Current/Resources/MacOS/JavaApplicationStub"; */ private File javaApplicationStub; /** * The main class to execute when double-clicking the Application Bundle * * @parameter expression="${mainClass}" * @required */ private String mainClass; /** * The name of the Bundle. This is the name that is given to the application bundle; * and it is also what will show up in the application menu, dock etc. * * @parameter default-value="${project.name}" * @required */ private String bundleName; /** * The icon file for the bundle * * @parameter */ private File iconFile; /** * The version of the project. Will be used as the value of the CFBundleVersion key. * * @parameter default-value="${project.version}" */ private String version; /** * A value for the JVMVersion key. * * @parameter default-value="1.4+" */ private String jvmVersion; /** * The location of the produced Zip file containing the bundle. * * @parameter default-value="${project.build.directory}/${project.build.finalName}-app.zip" */ private File zipFile; /** * Paths to be put on the classpath in addition to the projects dependencies. * Might be useful to specifiy locations of dependencies in the provided scope that are not distributed with * the bundle but have a known location on the system. * {@see http://jira.codehaus.org/browse/MOJO-874} * * @parameter */ private List additionalClasspath; /** * Additional resources (as a list of FileSet objects) that will be copies into * the build directory and included in the .dmg and zip files alongside with the * application bundle. * * @parameter */ private List additionalResources; /** * Velocity Component. * * @component * @readonly */ private VelocityComponent velocity; /** * The location of the template for Info.plist. * Classpath is checked before the file system. * * @parameter default-value="org/codehaus/mojo/osxappbundle/Info.plist.template" */ private String dictionaryFile; /** * Options to the JVM, will be used as the value of VMOptions in Info.plist. * * @parameter */ private String vmOptions; /** * The Zip archiver. * * @component * @readonly */ private MavenProjectHelper projectHelper; /** * The Zip archiver. * * @parameter expression="${component.org.codehaus.plexus.archiver.Archiver#zip}" * @required * @readonly */ private ZipArchiver zipArchiver; /** * If this is set to <code>true</code>, the generated DMG file will be internet-enabled. * The default is ${false} * * @parameter default-value="false" */ private boolean internetEnable; /** * Comma separated list of ArtifactIds to exclude from the dependency copy. * @parameter default-value="" */ private Set excludeArtifactIds; /** * The path to the SetFile tool. */ private static final String SET_FILE_PATH = "/Developer/Tools/SetFile"; /** * Bundle project as a Mac OS X application bundle. * * @throws MojoExecutionException If an unexpected error occurs during packaging of the bundle. */ public void execute() throws MojoExecutionException { // Set up and create directories buildDirectory.mkdirs(); File bundleDir = new File( buildDirectory, cleanBundleName(bundleName) + ".app" ); bundleDir.mkdirs(); File contentsDir = new File( bundleDir, "Contents" ); contentsDir.mkdirs(); File resourcesDir = new File( contentsDir, "Resources" ); resourcesDir.mkdirs(); File javaDirectory = new File( resourcesDir, "Java" ); javaDirectory.mkdirs(); File macOSDirectory = new File( contentsDir, "MacOS" ); macOSDirectory.mkdirs(); // Copy in the native java application stub File stub = new File( macOSDirectory, javaApplicationStub.getName() ); if(! javaApplicationStub.exists()) { String message = "Can't find JavaApplicationStub binary. File does not exist: " + javaApplicationStub; if(! isOsX() ) { message += "\nNOTICE: You are running the osxappbundle plugin on a non OS X platform. To make this work you need to copy the JavaApplicationStub binary into your source tree. Then configure it with the 'javaApplicationStub' configuration property.\nOn an OS X machine, the JavaApplicationStub is typically located under /System/Library/Frameworks/JavaVM.framework/Versions/Current/Resources/MacOS/JavaApplicationStub"; } throw new MojoExecutionException( message); } else { try { FileUtils.copyFile( javaApplicationStub, stub ); } catch ( IOException e ) { throw new MojoExecutionException( "Could not copy file " + javaApplicationStub + " to directory " + macOSDirectory, e ); } } // Copy icon file to the bundle if specified if ( iconFile != null ) { try { FileUtils.copyFileToDirectory( iconFile, resourcesDir ); } catch ( IOException e ) { throw new MojoExecutionException( "Error copying file " + iconFile + " to " + resourcesDir, e ); } } // Resolve and copy in all dependecies from the pom List files = copyDependencies( javaDirectory ); // Create and write the Info.plist file File infoPlist = new File( bundleDir, "Contents/Info.plist" ); writeInfoPlist( infoPlist, files ); // Copy specified additional resources into the top level directory if (additionalResources != null && !additionalResources.isEmpty()) { copyResources( additionalResources ); } if ( isOsX() ) { // Make the stub executable Commandline chmod = new Commandline(); try { chmod.setExecutable( "chmod" ); chmod.createArgument().setValue( "755" ); chmod.createArgument().setValue( stub.getAbsolutePath() ); chmod.execute(); } catch ( CommandLineException e ) { throw new MojoExecutionException( "Error executing " + chmod + " ", e ); } // This makes sure that the .app dir is actually registered as an application bundle if ( new File( SET_FILE_PATH ).exists() ) { Commandline setFile = new Commandline(); try { setFile.setExecutable(SET_FILE_PATH); setFile.createArgument().setValue( "-a" ); setFile.createArgument().setValue( "B" ); setFile.createArgument().setValue( bundleDir.getAbsolutePath() ); setFile.execute(); } catch ( CommandLineException e ) { throw new MojoExecutionException( "Error executing " + setFile, e ); } } else { getLog().warn( "Could not set 'Has Bundle' attribute. " +SET_FILE_PATH +" not found, is Developer Tools installed?" ); } // Create a .dmg file of the app Commandline dmg = new Commandline(); try { dmg.setExecutable( "hdiutil" ); dmg.createArgument().setValue( "create" ); dmg.createArgument().setValue( "-srcfolder" ); dmg.createArgument().setValue( buildDirectory.getAbsolutePath() ); dmg.createArgument().setValue( diskImageFile.getAbsolutePath() ); try { dmg.execute().waitFor(); } catch ( InterruptedException e ) { throw new MojoExecutionException( "Thread was interrupted while creating DMG " + diskImageFile, e ); } } catch ( CommandLineException e ) { throw new MojoExecutionException( "Error creating disk image " + diskImageFile, e ); } if(internetEnable) { try { Commandline internetEnable = new Commandline(); internetEnable.setExecutable("hdiutil"); internetEnable.createArgument().setValue("internet-enable" ); internetEnable.createArgument().setValue("-yes"); internetEnable.createArgument().setValue(diskImageFile.getAbsolutePath()); internetEnable.execute(); } catch (CommandLineException e) { throw new MojoExecutionException("Error internet enabling disk image: " + diskImageFile, e); } } projectHelper.attachArtifact(project, "dmg", null, diskImageFile); } zipArchiver.setDestFile( zipFile ); try { String[] stubPattern = {buildDirectory.getName() + "/" + bundleDir.getName() +"/Contents/MacOS/" + javaApplicationStub.getName()}; zipArchiver.addDirectory( buildDirectory.getParentFile(), new String[]{buildDirectory.getName() + "/**"}, stubPattern); DirectoryScanner scanner = new DirectoryScanner(); scanner.setBasedir( buildDirectory.getParentFile() ); scanner.setIncludes( stubPattern); scanner.scan(); String[] stubs = scanner.getIncludedFiles(); for ( int i = 0; i < stubs.length; i++ ) { String s = stubs[i]; zipArchiver.addFile( new File( buildDirectory.getParentFile(), s ), s, 0755 ); } zipArchiver.createArchive(); projectHelper.attachArtifact(project, "zip", null, zipFile); } catch ( ArchiverException e ) { throw new MojoExecutionException( "Could not create zip archive of application bundle in " + zipFile, e ); } catch ( IOException e ) { throw new MojoExecutionException( "IOException creating zip archive of application bundle in " + zipFile, e ); } } /** * The bundle name is used in paths, so we need to clean it for * unwanted characters, like ":" on Windows. * @param bundleName the "unclean" bundle name. * @return a clean bundle name */ private String cleanBundleName(String bundleName) { return bundleName.replace(':', '-'); } private boolean isOsX() { return System.getProperty( "mrj.version" ) != null; } /** * Copy all dependencies into the $JAVAROOT directory * * @param javaDirectory where to put jar files * @return A list of file names added * @throws MojoExecutionException */ private List copyDependencies( File javaDirectory ) throws MojoExecutionException { ArtifactRepositoryLayout layout = new DefaultRepositoryLayout(); List list = new ArrayList(); File repoDirectory = new File(javaDirectory, "repo"); repoDirectory.mkdirs(); // First, copy the project's own artifact File artifactFile = project.getArtifact().getFile(); // Pom modules have no artifact file if(artifactFile != null) { list.add( repoDirectory.getName() +"/" +layout.pathOf(project.getArtifact())); try { FileUtils.copyFile( artifactFile, new File(repoDirectory, layout.pathOf(project.getArtifact())) ); } catch ( IOException e ) { throw new MojoExecutionException( "Could not copy artifact file " + artifactFile + " to " + javaDirectory ); } } Set artifacts = project.getArtifacts(); Iterator i = artifacts.iterator(); while ( i.hasNext() ) { Artifact artifact = (Artifact) i.next(); String artifactId = artifact.getArtifactId(); if (excludeArtifactIds != null && excludeArtifactIds.contains(artifactId)) { getLog().info( "Skipping excluded artifact: " + artifact.toString() ); continue; } File file = artifact.getFile(); File dest = new File(repoDirectory, layout.pathOf(artifact)); getLog().debug( "Adding " + file ); try { FileUtils.copyFile( file, dest); } catch ( IOException e ) { throw new MojoExecutionException( "Error copying file " + file + " into " + javaDirectory, e ); } list.add( repoDirectory.getName() +"/" + layout.pathOf(artifact) ); } return list; } /** * Writes an Info.plist file describing this bundle. * * @param infoPlist The file to write Info.plist contents to * @param files A list of file names of the jar files to add in $JAVAROOT * @throws MojoExecutionException */ private void writeInfoPlist( File infoPlist, List files ) throws MojoExecutionException { VelocityContext velocityContext = new VelocityContext(); velocityContext.put( "mainClass", mainClass ); velocityContext.put( "cfBundleExecutable", javaApplicationStub.getName()); velocityContext.put( "vmOptions", vmOptions); velocityContext.put( "bundleName", cleanBundleName(bundleName) ); velocityContext.put( "iconFile", iconFile == null ? "GenericJavaApp.icns" : iconFile.getName() ); velocityContext.put( "version", version ); velocityContext.put( "jvmVersion", jvmVersion ); StringBuffer jarFilesBuffer = new StringBuffer(); jarFilesBuffer.append( "<array>" ); for ( int i = 0; i < files.size(); i++ ) { String name = (String) files.get( i ); jarFilesBuffer.append( "<string>" ); jarFilesBuffer.append( "$JAVAROOT/" ).append( name ); jarFilesBuffer.append( "</string>" ); } if ( additionalClasspath != null ) { for ( int i = 0; i < additionalClasspath.size(); i++ ) { String pathElement = (String) additionalClasspath.get( i ); jarFilesBuffer.append( "<string>" ); jarFilesBuffer.append( pathElement ); jarFilesBuffer.append( "</string>" ); } } jarFilesBuffer.append( "</array>" ); velocityContext.put( "classpath", jarFilesBuffer.toString() ); try { String encoding = detectEncoding(dictionaryFile, velocityContext); getLog().debug( "Detected encoding " + encoding + " for dictionary file " +dictionaryFile ); Writer writer = new OutputStreamWriter( new FileOutputStream(infoPlist), encoding ); velocity.getEngine().mergeTemplate( dictionaryFile, encoding, velocityContext, writer ); writer.close(); } catch ( IOException e ) { throw new MojoExecutionException( "Could not write Info.plist to file " + infoPlist, e ); } catch ( ParseErrorException e ) { throw new MojoExecutionException( "Error parsing " + dictionaryFile, e ); } catch ( ResourceNotFoundException e ) { throw new MojoExecutionException( "Could not find resource for template " + dictionaryFile, e ); } catch ( MethodInvocationException e ) { throw new MojoExecutionException( "MethodInvocationException occured merging Info.plist template " + dictionaryFile, e ); } catch ( Exception e ) { throw new MojoExecutionException( "Exception occured merging Info.plist template " + dictionaryFile, e ); } } private String detectEncoding( String dictionaryFile, VelocityContext velocityContext ) throws Exception { StringWriter sw = new StringWriter(); velocity.getEngine().mergeTemplate( dictionaryFile, "utf-8", velocityContext, sw ); return new DefaultEncodingDetector().detectXmlEncoding( new ByteArrayInputStream(sw.toString().getBytes( "utf-8" )) ); } /** * Copies given resources to the build directory. * * @param fileSets A list of FileSet objects that represent additional resources to copy. * @throws MojoExecutionException In case af a resource copying error. */ private void copyResources( List fileSets ) throws MojoExecutionException { final String[] emptyStrArray = {}; for ( Iterator it = fileSets.iterator(); it.hasNext(); ) { FileSet fileSet = (FileSet) it.next(); File resourceDirectory = new File( fileSet.getDirectory() ); if ( !resourceDirectory.isAbsolute() ) { resourceDirectory = new File( project.getBasedir(), resourceDirectory.getPath() ); } if ( !resourceDirectory.exists() ) { getLog().info( "Additional resource directory does not exist: " + resourceDirectory ); continue; } DirectoryScanner scanner = new DirectoryScanner(); scanner.setBasedir( resourceDirectory ); if ( fileSet.getIncludes() != null && !fileSet.getIncludes().isEmpty() ) { scanner.setIncludes( (String[]) fileSet.getIncludes().toArray( emptyStrArray ) ); } else { scanner.setIncludes( DEFAULT_INCLUDES ); } if ( fileSet.getExcludes() != null && !fileSet.getExcludes().isEmpty() ) { scanner.setExcludes( (String[]) fileSet.getExcludes().toArray( emptyStrArray ) ); } if (fileSet.isUseDefaultExcludes()) { scanner.addDefaultExcludes(); } scanner.scan(); List includedFiles = Arrays.asList( scanner.getIncludedFiles() ); getLog().info( "Copying " + includedFiles.size() + " additional resource" + ( includedFiles.size() > 1 ? "s" : "" ) ); for ( Iterator j = includedFiles.iterator(); j.hasNext(); ) { String destination = (String) j.next(); File source = new File( resourceDirectory, destination ); File destinationFile = new File( buildDirectory, destination ); if ( !destinationFile.getParentFile().exists() ) { destinationFile.getParentFile().mkdirs(); } try { FileUtils.copyFile(source, destinationFile); } catch ( IOException e ) { throw new MojoExecutionException( "Error copying additional resource " + source, e ); } } } } }