View Javadoc

1   package org.codehaus.mojo.osxappbundle;
2   
3   /*
4    * Copyright 2001-2008 The Codehaus.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *      http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  
20  import org.apache.maven.artifact.Artifact;
21  import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout;
22  import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout;
23  import org.apache.maven.plugin.AbstractMojo;
24  import org.apache.maven.plugin.MojoExecutionException;
25  import org.apache.maven.project.MavenProject;
26  import org.apache.maven.project.MavenProjectHelper;
27  import org.apache.velocity.VelocityContext;
28  import org.apache.velocity.exception.MethodInvocationException;
29  import org.apache.velocity.exception.ParseErrorException;
30  import org.apache.velocity.exception.ResourceNotFoundException;
31  import org.codehaus.plexus.archiver.ArchiverException;
32  import org.codehaus.plexus.archiver.zip.ZipArchiver;
33  import org.codehaus.plexus.util.DirectoryScanner;
34  import org.codehaus.plexus.util.FileUtils;
35  import org.codehaus.plexus.util.cli.CommandLineException;
36  import org.codehaus.plexus.util.cli.Commandline;
37  import org.codehaus.plexus.velocity.VelocityComponent;
38  import org.codehaus.mojo.osxappbundle.encoding.DefaultEncodingDetector;
39  
40  import java.io.File;
41  import java.io.FileWriter;
42  import java.io.IOException;
43  import java.io.StringWriter;
44  import java.io.ByteArrayInputStream;
45  import java.io.Writer;
46  import java.io.OutputStreamWriter;
47  import java.io.FileOutputStream;
48  import java.util.ArrayList;
49  import java.util.Iterator;
50  import java.util.List;
51  import java.util.Set;
52  import java.util.Arrays;
53  
54  /**
55   * Package dependencies as an Application Bundle for Mac OS X.
56   *
57   * @goal bundle
58   * @phase package
59   * @requiresDependencyResolution runtime
60   */
61  public class CreateApplicationBundleMojo
62      extends AbstractMojo
63  {
64  
65      /**
66       * Default includes - everything is included.
67       */
68      private static final String[] DEFAULT_INCLUDES = {"**/**"};
69  
70      /**
71       * The Maven Project Object
72       *
73       * @parameter default-value="${project}"
74       * @readonly
75       */
76      private MavenProject project;
77  
78      /**
79       * The directory where the application bundle will be created
80       *
81       * @parameter default-value="${project.build.directory}/${project.build.finalName}";
82       */
83      private File buildDirectory;
84  
85      /**
86       * The location of the generated disk image file
87       *
88       * @parameter default-value="${project.build.directory}/${project.build.finalName}.dmg"
89       */
90      private File diskImageFile;
91  
92  
93      /**
94       * The location of the Java Application Stub
95       *
96       * @parameter default-value="/System/Library/Frameworks/JavaVM.framework/Versions/Current/Resources/MacOS/JavaApplicationStub";
97       */
98      private File javaApplicationStub;
99  
100     /**
101      * The main class to execute when double-clicking the Application Bundle
102      *
103      * @parameter expression="${mainClass}"
104      * @required
105      */
106     private String mainClass;
107 
108     /**
109      * The name of the Bundle. This is the name that is given to the application bundle;
110      * and it is also what will show up in the application menu, dock etc.
111      *
112      * @parameter default-value="${project.name}"
113      * @required
114      */
115     private String bundleName;
116 
117 
118     /**
119      * The icon file for the bundle
120      *
121      * @parameter
122      */
123     private File iconFile;
124 
125     /**
126      * The version of the project. Will be used as the value of the CFBundleVersion key.
127      *
128      * @parameter default-value="${project.version}"
129      */
130     private String version;
131 
132     /**
133      * A value for the JVMVersion key.
134      *
135      * @parameter default-value="1.4+"
136      */
137     private String jvmVersion;
138 
139     /**
140      * The location of the produced Zip file containing the bundle.
141      *
142      * @parameter default-value="${project.build.directory}/${project.build.finalName}-app.zip"
143      */
144     private File zipFile;
145 
146     /**
147      * Paths to be put on the classpath in addition to the projects dependencies.
148      * Might be useful to specifiy locations of dependencies in the provided scope that are not distributed with
149      * the bundle but have a known location on the system.
150      * {@see http://jira.codehaus.org/browse/MOJO-874}
151      *
152      * @parameter
153      */
154     private List additionalClasspath;
155 
156     /**
157      * Additional resources (as a list of FileSet objects) that will be copies into
158      * the build directory and included in the .dmg and zip files alongside with the
159      * application bundle.
160      *
161      * @parameter
162      */
163     private List additionalResources;
164 
165     /**
166      * Velocity Component.
167      *
168      * @component
169      * @readonly
170      */
171     private VelocityComponent velocity;
172 
173     /**
174      * The location of the template for Info.plist.
175      * Classpath is checked before the file system.
176      *
177      * @parameter default-value="org/codehaus/mojo/osxappbundle/Info.plist.template"
178      */
179     private String dictionaryFile;
180 
181     /**
182      * Options to the JVM, will be used as the value of VMOptions in Info.plist.
183      *
184      * @parameter
185      */
186     private String vmOptions;
187 
188 
189     /**
190      * The Zip archiver.
191      *
192      * @component
193      * @readonly
194      */
195     private MavenProjectHelper projectHelper;
196 
197     /**
198      * The Zip archiver.
199      *
200      * @parameter expression="${component.org.codehaus.plexus.archiver.Archiver#zip}"
201      * @required
202      * @readonly
203      */
204     private ZipArchiver zipArchiver;
205 
206     /**
207      * If this is set to <code>true</code>, the generated DMG file will be internet-enabled.
208      * The default is ${false}
209      *
210      * @parameter default-value="false"
211      */
212     private boolean internetEnable;
213 
214     /**
215      * The path to the SetFile tool.
216      */
217     private static final String SET_FILE_PATH = "/Developer/Tools/SetFile";
218 
219 
220     /**
221      * Bundle project as a Mac OS X application bundle.
222      *
223      * @throws MojoExecutionException If an unexpected error occurs during packaging of the bundle.
224      */
225     public void execute()
226         throws MojoExecutionException
227     {
228 
229         // Set up and create directories
230         buildDirectory.mkdirs();
231 
232         File bundleDir = new File( buildDirectory, bundleName + ".app" );
233         bundleDir.mkdirs();
234 
235         File contentsDir = new File( bundleDir, "Contents" );
236         contentsDir.mkdirs();
237 
238         File resourcesDir = new File( contentsDir, "Resources" );
239         resourcesDir.mkdirs();
240 
241         File javaDirectory = new File( resourcesDir, "Java" );
242         javaDirectory.mkdirs();
243 
244         File macOSDirectory = new File( contentsDir, "MacOS" );
245         macOSDirectory.mkdirs();
246 
247         // Copy in the native java application stub
248         File stub = new File( macOSDirectory, javaApplicationStub.getName() );
249         if(! javaApplicationStub.exists()) {
250             String message = "Can't find JavaApplicationStub binary. File does not exist: " + javaApplicationStub;
251 
252             if(! isOsX() ) {
253                 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";
254             }
255 
256             throw new MojoExecutionException( message);
257             
258         } else {
259             try
260             {
261                 FileUtils.copyFile( javaApplicationStub, stub );
262             }
263             catch ( IOException e )
264             {
265                 throw new MojoExecutionException(
266                     "Could not copy file " + javaApplicationStub + " to directory " + macOSDirectory, e );
267             }
268         }
269 
270         // Copy icon file to the bundle if specified
271         if ( iconFile != null )
272         {
273             try
274             {
275                 FileUtils.copyFileToDirectory( iconFile, resourcesDir );
276             }
277             catch ( IOException e )
278             {
279                 throw new MojoExecutionException( "Error copying file " + iconFile + " to " + resourcesDir, e );
280             }
281         }
282 
283         // Resolve and copy in all dependecies from the pom
284         List files = copyDependencies( javaDirectory );
285 
286         // Create and write the Info.plist file
287         File infoPlist = new File( bundleDir, "Contents/Info.plist" );
288         writeInfoPlist( infoPlist, files );
289 
290         // Copy specified additional resources into the top level directory
291         if (additionalResources != null && !additionalResources.isEmpty())
292         {
293             copyResources( additionalResources );
294         }
295 
296         if ( isOsX() )
297         {
298             // Make the stub executable
299             Commandline chmod = new Commandline();
300             try
301             {
302                 chmod.setExecutable( "chmod" );
303                 chmod.createArgument().setValue( "755" );
304                 chmod.createArgument().setValue( stub.getAbsolutePath() );
305 
306                 chmod.execute();
307             }
308             catch ( CommandLineException e )
309             {
310                 throw new MojoExecutionException( "Error executing " + chmod + " ", e );
311             }
312 
313             // This makes sure that the .app dir is actually registered as an application bundle
314             if ( new File( SET_FILE_PATH ).exists() )
315             {
316                 Commandline setFile = new Commandline();
317                 try
318                 {
319                     setFile.setExecutable(SET_FILE_PATH);
320                     setFile.createArgument().setValue( "-a B" );
321                     setFile.createArgument().setValue( bundleDir.getAbsolutePath() );
322 
323                     setFile.execute();
324                 }
325                 catch ( CommandLineException e )
326                 {
327                     throw new MojoExecutionException( "Error executing " + setFile, e );
328                 }
329             }
330             else
331             {
332                 getLog().warn( "Could  not set 'Has Bundle' attribute. " +SET_FILE_PATH +" not found, is Developer Tools installed?" );
333             }
334             // Create a .dmg file of the app
335             Commandline dmg = new Commandline();
336             try
337             {
338                 dmg.setExecutable( "hdiutil" );
339                 dmg.createArgument().setValue( "create" );
340                 dmg.createArgument().setValue( "-srcfolder" );
341                 dmg.createArgument().setValue( buildDirectory.getAbsolutePath() );
342                 dmg.createArgument().setValue( diskImageFile.getAbsolutePath() );
343                 try
344                 {
345                     dmg.execute().waitFor();
346                 }
347                 catch ( InterruptedException e )
348                 {
349                     throw new MojoExecutionException( "Thread was interrupted while creating DMG " + diskImageFile, e );
350                 }
351             }
352             catch ( CommandLineException e )
353             {
354                 throw new MojoExecutionException( "Error creating disk image " + diskImageFile, e );
355             }
356             if(internetEnable) {
357                 try {
358 
359                     Commandline internetEnable = new Commandline();
360 
361                     internetEnable.setExecutable("hdiutil");
362                     internetEnable.createArgument().setValue("internet-enable" );
363                     internetEnable.createArgument().setValue("-yes");
364                     internetEnable.createArgument().setValue(diskImageFile.getAbsolutePath());
365 
366                     internetEnable.execute();
367                 } catch (CommandLineException e) {
368                     throw new MojoExecutionException("Error internet enabling disk image: " + diskImageFile, e);
369                 }
370             }
371             projectHelper.attachArtifact(project, "dmg", null, diskImageFile);
372         }
373 
374         zipArchiver.setDestFile( zipFile );
375         try
376         {
377             String[] stubPattern = {buildDirectory.getName() + "/" + bundleDir.getName() +"/Contents/MacOS/"
378                                     + javaApplicationStub.getName()};
379 
380             zipArchiver.addDirectory( buildDirectory.getParentFile(), new String[]{buildDirectory.getName() + "/**"},
381                     stubPattern);
382 
383             DirectoryScanner scanner = new DirectoryScanner();
384             scanner.setBasedir( buildDirectory.getParentFile() );
385             scanner.setIncludes( stubPattern);
386             scanner.scan();
387 
388             String[] stubs = scanner.getIncludedFiles();
389             for ( int i = 0; i < stubs.length; i++ )
390             {
391                 String s = stubs[i];
392                 zipArchiver.addFile( new File( buildDirectory.getParentFile(), s ), s, 0755 );
393             }
394 
395             zipArchiver.createArchive();
396             projectHelper.attachArtifact(project, "zip", null, zipFile);
397         }
398         catch ( ArchiverException e )
399         {
400             throw new MojoExecutionException( "Could not create zip archive of application bundle in " + zipFile, e );
401         }
402         catch ( IOException e )
403         {
404             throw new MojoExecutionException( "IOException creating zip archive of application bundle in " + zipFile,
405                                               e );
406         }
407 
408 
409     }
410 
411     private boolean isOsX()
412     {
413         return System.getProperty( "mrj.version" ) != null;
414     }
415 
416     /**
417      * Copy all dependencies into the $JAVAROOT directory
418      *
419      * @param javaDirectory where to put jar files
420      * @return A list of file names added
421      * @throws MojoExecutionException
422      */
423     private List copyDependencies( File javaDirectory )
424         throws MojoExecutionException
425     {
426 
427         ArtifactRepositoryLayout layout = new DefaultRepositoryLayout();
428 
429         List list = new ArrayList();
430 
431         File repoDirectory = new File(javaDirectory, "repo");
432         repoDirectory.mkdirs();
433 
434         // First, copy the project's own artifact
435         File artifactFile = project.getArtifact().getFile();
436         list.add( repoDirectory.getName() +"/" +layout.pathOf(project.getArtifact()));
437 
438         try
439         {
440             FileUtils.copyFile( artifactFile, new File(repoDirectory, layout.pathOf(project.getArtifact())) );
441         }
442         catch ( IOException e )
443         {
444             throw new MojoExecutionException( "Could not copy artifact file " + artifactFile + " to " + javaDirectory );
445         }
446 
447         Set artifacts = project.getArtifacts();
448 
449         Iterator i = artifacts.iterator();
450 
451         while ( i.hasNext() )
452         {
453             Artifact artifact = (Artifact) i.next();
454 
455             File file = artifact.getFile();
456             File dest = new File(repoDirectory, layout.pathOf(artifact));
457 
458             getLog().debug( "Adding " + file );
459 
460             try
461             {
462                 FileUtils.copyFile( file, dest);
463             }
464             catch ( IOException e )
465             {
466                 throw new MojoExecutionException( "Error copying file " + file + " into " + javaDirectory, e );
467             }
468 
469             list.add( repoDirectory.getName() +"/" + layout.pathOf(artifact) );
470         }
471 
472         return list;
473 
474     }
475 
476     /**
477      * Writes an Info.plist file describing this bundle.
478      *
479      * @param infoPlist The file to write Info.plist contents to
480      * @param files     A list of file names of the jar files to add in $JAVAROOT
481      * @throws MojoExecutionException
482      */
483     private void writeInfoPlist( File infoPlist, List files )
484         throws MojoExecutionException
485     {
486 
487         VelocityContext velocityContext = new VelocityContext();
488 
489         velocityContext.put( "mainClass", mainClass );
490         velocityContext.put( "cfBundleExecutable", javaApplicationStub.getName());
491         velocityContext.put( "vmOptions", vmOptions);
492         velocityContext.put( "bundleName", bundleName );
493 
494         velocityContext.put( "iconFile", iconFile == null ? "GenericJavaApp.icns" : iconFile.getName() );
495 
496         velocityContext.put( "version", version );
497 
498         velocityContext.put( "jvmVersion", jvmVersion );
499 
500         StringBuffer jarFilesBuffer = new StringBuffer();
501 
502         jarFilesBuffer.append( "<array>" );
503         for ( int i = 0; i < files.size(); i++ )
504         {
505             String name = (String) files.get( i );
506             jarFilesBuffer.append( "<string>" );
507             jarFilesBuffer.append( "$JAVAROOT/" ).append( name );
508             jarFilesBuffer.append( "</string>" );
509 
510         }
511         if ( additionalClasspath != null )
512         {
513             for ( int i = 0; i < additionalClasspath.size(); i++ )
514             {
515                 String pathElement = (String) additionalClasspath.get( i );
516                 jarFilesBuffer.append( "<string>" );
517                 jarFilesBuffer.append( pathElement );
518                 jarFilesBuffer.append( "</string>" );
519 
520             }
521         }
522         jarFilesBuffer.append( "</array>" );
523 
524         velocityContext.put( "classpath", jarFilesBuffer.toString() );
525 
526         try
527         {
528 
529             String encoding = detectEncoding(dictionaryFile, velocityContext);
530 
531             getLog().debug( "Detected encoding " + encoding + " for dictionary file " +dictionaryFile  );
532 
533             Writer writer = new OutputStreamWriter( new FileOutputStream(infoPlist), encoding );
534 
535             velocity.getEngine().mergeTemplate( dictionaryFile, encoding, velocityContext, writer );
536 
537             writer.close();
538         }
539         catch ( IOException e )
540         {
541             throw new MojoExecutionException( "Could not write Info.plist to file " + infoPlist, e );
542         }
543         catch ( ParseErrorException e )
544         {
545             throw new MojoExecutionException( "Error parsing " + dictionaryFile, e );
546         }
547         catch ( ResourceNotFoundException e )
548         {
549             throw new MojoExecutionException( "Could not find resource for template " + dictionaryFile, e );
550         }
551         catch ( MethodInvocationException e )
552         {
553             throw new MojoExecutionException(
554                 "MethodInvocationException occured merging Info.plist template " + dictionaryFile, e );
555         }
556         catch ( Exception e )
557         {
558             throw new MojoExecutionException( "Exception occured merging Info.plist template " + dictionaryFile, e );
559         }
560 
561     }
562 
563     private String detectEncoding( String dictionaryFile, VelocityContext velocityContext )
564         throws Exception
565     {
566         StringWriter sw = new StringWriter();
567         velocity.getEngine().mergeTemplate( dictionaryFile, "utf-8", velocityContext, sw );
568         return new DefaultEncodingDetector().detectXmlEncoding( new ByteArrayInputStream(sw.toString().getBytes( "utf-8" )) );
569     }
570 
571     /**
572      * Copies given resources to the build directory. 
573      *
574      * @param fileSets A list of FileSet objects that represent additional resources to copy.
575      * @throws MojoExecutionException In case af a resource copying error.
576      */
577     private void copyResources( List fileSets )
578         throws MojoExecutionException
579     {
580         final String[] emptyStrArray = {};
581         
582         for ( Iterator it = fileSets.iterator(); it.hasNext(); )
583         {
584             FileSet fileSet = (FileSet) it.next();
585 
586             File resourceDirectory = new File( fileSet.getDirectory() );
587             if ( !resourceDirectory.isAbsolute() )
588             {
589                 resourceDirectory = new File( project.getBasedir(), resourceDirectory.getPath() );
590             }
591 
592             if ( !resourceDirectory.exists() )
593             {
594                 getLog().info( "Additional resource directory does not exist: " + resourceDirectory );
595                 continue;
596             }
597 
598             DirectoryScanner scanner = new DirectoryScanner();
599 
600             scanner.setBasedir( resourceDirectory );
601             if ( fileSet.getIncludes() != null && !fileSet.getIncludes().isEmpty() )
602             {
603                 scanner.setIncludes( (String[]) fileSet.getIncludes().toArray( emptyStrArray ) );
604             }
605             else
606             {
607                 scanner.setIncludes( DEFAULT_INCLUDES );
608             }
609 
610             if ( fileSet.getExcludes() != null && !fileSet.getExcludes().isEmpty() )
611             {
612                 scanner.setExcludes( (String[]) fileSet.getExcludes().toArray( emptyStrArray ) );
613             }
614 
615             if (fileSet.isUseDefaultExcludes())
616             {
617                 scanner.addDefaultExcludes();
618             }
619 
620             scanner.scan();
621 
622             List includedFiles = Arrays.asList( scanner.getIncludedFiles() );
623 
624             getLog().info( "Copying " + includedFiles.size() + " additional resource"
625                            + ( includedFiles.size() > 1 ? "s" : "" ) );
626 
627             for ( Iterator j = includedFiles.iterator(); j.hasNext(); )
628             {
629                 String destination = (String) j.next();
630                 File source = new File( resourceDirectory, destination );
631                 File destinationFile = new File( buildDirectory, destination );
632 
633                 if ( !destinationFile.getParentFile().exists() )
634                 {
635                     destinationFile.getParentFile().mkdirs();
636                 }
637 
638                 try
639                 {
640                     FileUtils.copyFile(source, destinationFile);
641                 }
642                 catch ( IOException e )
643                 {
644                     throw new MojoExecutionException( "Error copying additional resource " + source, e );
645                 }
646             }
647         }
648     }
649 
650 }