/* * Copyright (c) 2015, UltraMixer Digital Audio Solutions <info@ultramixer.com>, Seth J. Morabito <sethm@loomcom.com> * All rights reserved. * * 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. */ package com.ultramixer.jarbundler; // This package's imports // Java I/O import java.io.BufferedWriter; import java.io.BufferedReader; import java.io.File; import java.io.FileWriter; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; // Java Utility import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; // Apache Jakarta import org.apache.tools.ant.BuildException; import org.apache.tools.ant.FileScanner; import org.apache.tools.ant.Project; import org.apache.tools.ant.types.FileList; import org.apache.tools.ant.types.FileSet; import org.apache.tools.ant.types.PatternSet; import org.apache.tools.ant.taskdefs.MatchingTask; import org.apache.tools.ant.taskdefs.Chmod; import org.apache.tools.ant.taskdefs.Delete; import org.apache.tools.ant.util.FileUtils; // Java language imports import java.lang.Boolean; import java.lang.String; import java.lang.System; /** * <p> * An ant task which creates a Mac OS X Application Bundle for a Java * application. * </p> * * <dl> * <dt>dir</dt> * <dd>The directory into which to put the new application bundle.</dd> * <dt>name</dt> * <dd>The name of the application bundle. Note that the maximum length of this * name is 16 characters, and it will be silently cropped if it is longer than * this.</dd> * <dt>mainclass</dt> * <dd>The main Java class to call when running the application.</dd> * </dl> * * <p> * One of the following three MUST be used: * * <ol> * <li>jars Space or comma-separated list of JAR files to include.; OR</li> * <li>One or more nested <jarfileset>s. These are normal ANT FileSets; * OR </li> * <li>One or more nested <jarfilelist>s. These are standard ANT * FileLists. </li> * </ol> * * <p> * Optional attributes: * * <p> * The following attributes are not required, but you can use them to override * default behavior. * * <dl> * <dt>verbose * <dd>If true, show more verbose output while running the task * * <dt>version * <dd>Version information about your application (e.g., "1.0") * </dl> * * These attributes control the fine-tuning of the "Mac OS X" look and feel. * * <dl> * <dt>arguments * <dd>Command line arguments. (no default) * * <dt>smalltabs * <dd>Use small tabs. (default "false") Deprecated under JVM 1.4.1 * * <dt>antialiasedgraphics * <dd>Use anti-aliased graphics (default "false") * * <dt>antialiasedtext * <dd>Use anti-aliased text (default "false") * * <dt>bundleid * <dd>Unique identifier for this bundle, in the form of a Java package. No * default. * * <dt>buildnumber * <dd>Unique identifier for this build * * <dt>developmentregion * <dd>Development Region. Default "English". * * <dt>execs * <dd>Files to be copied into "Resources/MacOS" and made executable * * <dt>liveresize * <dd>Use "Live resizing" (default "false") Deprecated under JVM 1.4.1 * * * <dt>growbox * <dd>Show growbox (default "true") * * <dt>growboxintrudes * <dd>Intruding growbox (default "false") Deprecated under JVM 1.4.1 * * <dt>screenmenu * <dd>Put swing menu into Mac OS X menu bar. * * <dt>type * <dd>Bundle type (default "APPL") * * <dt>signature * <dd>Bundle Signature (default "????") * * <dt>stubfile * <dd>The Java Application Stub file to copy for your application (default * MacOS system stub file) * </dl> * * <p> * Rarely used optional attributes. * <dl> * <dt>chmod * <dd>Full path to the chmod command. This almost certainly does NOT need to * be set. * </dl> * * <p> * The task also supports nested <execfileset> and/or <execfilelist> * elements, and <resourcefileset> and/or <resourcefilelist> * elements, which are standard Ant FileSet and FileList elements. In the first * case, the referenced files are copied to the <code>Contents/MacOS</code> * directory and made executable, and in the second they are copied to the * <code>Contents/Resources</code> directory and not made executable. If you * winrces, note that in fact the files are installed in locations which have * the same relation to the <code>Contents/Resources</code> directory as the * files in the FileSet or FileList have to the 'dir' attribute. Thus in the * case: * * <pre> * <resourcefileset dir="builddir/architectures" * includes="ppc/*.jnilib"/> * </pre> * * <p> * the <code>*.jnilib</code> files will be installed in * <code>Contents/Resources/ppc</code>. * * <p> * The task supports a nested <javaproperty> element, which allows you to * specify further properties which are set for the JVM when the application is * launched. This takes a required <code>key</code> attribute, giving the * property key, plus an attribute giving the property value, which may be one * of <code>value</code>, giving the string value of the property, * <code>file</code>, setting the value of the property to be the absolute * path of the given file, or <code>path</code>, which sets the value to the * given path. If you are setting paths here, recall that, within the bundle, * <code>$APP_PACKAGE</code> is set to the root directory of the bundle (ie, * the path to the <code>foo.app</code> directory), and <code>$JAVAROOT</code> * to the directory <code>Contents/Resources/Java</code>. * * <p> * Minimum example: * * <pre> * * <jarbundler dir="release" name="Bar Project" mainclass="org.bar.Main" * jars="bin/Bar.jar" /> * </pre> * * <p> * Using Filesets * * <pre> * <jarbundler dir="release" name="Bar Project" mainclass="org.bar.Main"> * <jarfileset dir="bin"> * <include name="*.jar" /> * <exclude name="test.jar" /> * </jarfileset> * <execfileset dir="execs"> * <include name="**" /> * </execfileset> * </jarbundler> * </pre> * * <p> * Much Longer example: * </p> * * <pre> * <jarbundler dir="release" * name="Foo Project" * mainclass="org.bar.Main" * version="1.0 b 1" * copyright="Foo Project (c) 2002" * type="APPL" * jars="bin/foo.jar bin/bar.jar" * execs="exec/foobar" * signature="????" * workingdirectory="temp" * icon="resources/foo.icns" * jvmversion="1.4.1+" * vmoptions="-Xmx256m"/> * </pre> * * http://developer.apple.com/documentation/MacOSX/Conceptual/BPRuntimeConfig/ */ public class JarBundler extends MatchingTask { private static final String DEFAULT_STUB = "/System/Library/Frameworks/JavaVM.framework/Versions/Current/Resources/MacOS/JavaApplicationStub"; private static final String ABOUTMENU_KEY = "com.apple.mrj.application.apple.menu.about.name"; private static final Set menuItems = new HashSet(); private File mAppIcon; private File mRootDir; private final List mJavaFileLists = new ArrayList(); private final List mJarFileSets = new ArrayList(); private final List mExecFileLists = new ArrayList(); private final List mExecFileSets = new ArrayList(); private final List mResourceFileLists = new ArrayList(); private final List mResourceFileSets = new ArrayList(); private final List mJarFileLists = new ArrayList(); private final List mJavaFileSets = new ArrayList(); private final List mExtraClassPathFileLists = new ArrayList(); private final List mExtraClassPathFileSets = new ArrayList(); private final List mJarAttrs = new ArrayList(); private final List mExecAttrs = new ArrayList(); private final List mExtraClassPathAttrs = new ArrayList(); private final List mHelpBooks = new ArrayList(); private boolean mVerbose = false; private boolean mShowPlist = false; // Java properties used by Mac OS X Java applications private File mStubFile = new File(DEFAULT_STUB); private Boolean mAntiAliasedGraphics = null; private Boolean mAntiAliasedText = null; private Boolean mLiveResize = null; private Boolean mScreenMenuBar = null; private Boolean mGrowbox = null; private Boolean mGrowboxIntrudes = null; // The root of the application bundle private File bundleDir; // "Contents" directory private File mContentsDir; // "Contents/MacOS" directory private File mMacOsDir; // "Contents/Resources" directory private File mResourcesDir; // "Contents/Resources/Java" directory private File mJavaDir; // Full path to the 'chmod' command. Can be overridden // with the 'chmod' attribute. Won't cause any harm if // not set, or if this executable doesn't exist. private AppBundleProperties bundleProperties = new AppBundleProperties(); // Ant file utilities private FileUtils mFileUtils = FileUtils.getFileUtils(); /*************************************************************************** * Retreive task attributes **************************************************************************/ /** * Arguments to the * * @param s * The arguments to pass to the application being launched. */ public void setArguments(String s) { bundleProperties.setArguments(s); } /** * Override the stub file path to build on non-MacOS platforms * * @param file * the path to the stub file */ public void setStubFile(File file) { mStubFile = (file.exists()) ? file : new File(DEFAULT_STUB); bundleProperties.setCFBundleExecutable(file.getName()); } /** * Setter for the "dir" attribute (required) */ public void setDir(File f) { mRootDir = f; } /** * Setter for the "name" attribute (required) This attribute names the * output application bundle and asks as the CFBundleName if 'bundlename' is * not specified */ public void setName(String s) { bundleProperties.setApplicationName(s); } /** * Setter for the "shortname" attribute (optional) This key identifies the * short name of the bundle. This name should be less than 16 characters * long and be suitable for displaying in the menu and the About box. The * name is (silently) cropped to this if necessary. */ public void setShortName(String s) { bundleProperties.setCFBundleName(s); } /** * Setter for the "mainclass" attribute (required) */ public void setMainClass(String s) { bundleProperties.setMainClass(s); } /** * Setter for the "WorkingDirectory" attribute (optional) */ public void setWorkingDirectory(String s) { bundleProperties.setWorkingDirectory(s); } /** * Setter for the "icon" attribute (optional) */ public void setIcon(File f) { mAppIcon = f; bundleProperties.setCFBundleIconFile(f.getName()); } /** * Setter for the "splashfile" attribute (optional). If it is somewhere * in a jar file, which contains a Splash-Screen manifest entry, * use "$JAVAROOT/myjar.jar" */ public void setSplashFile(String s) { bundleProperties.setSplashFile(s); } /** * Setter for the "bundleid" attribute (optional) This key specifies a * unique identifier string for the bundle. This identifier should be in the * form of a Java-style package name, for example com.mycompany.myapp. The * bundle identifier can be used to locate the bundle at runtime. The * preferences system uses this string to identify applications uniquely. * * No default. */ public void setBundleid(String s) { bundleProperties.setCFBundleIdentifier(s); } /** * Setter for the "developmentregion" attribute (optional) Default "English". */ public void setDevelopmentregion(String s) { bundleProperties.setCFBundleDevelopmentRegion(s); } /** Tobias Fischer, v2.3.0 * Setter for the "allowmixedlocalizations" attribute (optional) Default "false". */ public void setAllowMixedLocalizations(boolean b) { bundleProperties.setCFBundleAllowMixedLocalizations(b); } /** Tobias Fisher, v2.3.1 * Setter for the "NSHumanReadableCopyright" attribute (optional) */ public void setCopyright(String s) { bundleProperties.setNSHumanReadableCopyright(s); } /** Tobias Fischer, v2.4.0 * Setter for the "NSHighResolutionCapable" attribute (optional) Default "false". */ public void setHighResolutionCapable(boolean b) { bundleProperties.setNSHighResolutionCapable(b); } /** Adrien Quillet, v2.5.0 * Setter for the "NSPreferencesContentSize" attribute (optional). */ public void setContentSize(String s) { // Check input consistency String pattern = "[0-9]+,[0-9]+"; if(!s.matches(pattern)) { throw new BuildException("Invalid content size format (expected 'width,height')"); } bundleProperties.setNSPreferencesContentSize(s); } /** Tobias Fischer, v2.4.0 * Setter for the alternative 'JavaX' dictionary key */ public void setUseJavaXKey(boolean b) { if (b && (bundleProperties.getJavaVersion() >= 1.7)) { throw new BuildException("Setting usejavaxkey is useless if jvmversion is at least 1.7, because then the Oracle PList format is used"); } bundleProperties.setJavaXKey(b); } /** * Setter for the "smalltabs" attribute (optional) */ public void setSmallTabs(boolean b) { bundleProperties.addJavaProperty("com.apple.smallTabs", new Boolean(b) .toString()); } /** * Setter for the "vmoptions" attribute (optional) */ public void setVmoptions(String s) { bundleProperties.setVMOptions(s); } /** * Setter for the "antialiasedgraphics" attribute (optional) */ public void setAntialiasedgraphics(boolean b) { mAntiAliasedGraphics = new Boolean(b); } /** * Setter for the "antialiasedtext" attribute (optional) */ public void setAntialiasedtext(boolean b) { mAntiAliasedText = new Boolean(b); } /** * Setter for the "screenmenu" attribute (optional) */ public void setScreenmenu(boolean b) { mScreenMenuBar = new Boolean(b); } /** * Setter for the "growbox" attribute (optional) */ public void setGrowbox(boolean b) { mGrowbox = new Boolean(b); } /** * Setter for the "growboxintrudes" attribute (optional) */ public void setGrowboxintrudes(boolean b) { mGrowboxIntrudes = new Boolean(b); } /** * Setter for the "liveresize" attribute (optional) */ public void setLiveresize(boolean b) { mLiveResize = new Boolean(b); } /** * Setter for the "type" attribute (optional) */ public void setType(String s) { bundleProperties.setCFBundlePackageType(s); } /** * Setter for the "signature" attribute (optional) */ public void setSignature(String s) { bundleProperties.setCFBundleSignature(s); } /** * Setter for the "jvmversion" attribute (optional) */ public void setJvmversion(String s) { bundleProperties.setJVMVersion(s); if (bundleProperties.getJavaXKey() && (bundleProperties.getJavaVersion() >= 1.7)) { throw new BuildException("Setting usejavaxkey is useless if jvmversion is at least 1.7, because then the Oracle PList format is used"); } } // New in JarBundler 2.2.0; Tobias Bley ---------------- /** * Setter for the "JVMArchs" attribute (optional) */ public void setJvmArchs(String s) { bundleProperties.setJVMArchs(s); } /** Michael Bader <nufan_k@me.com> -------------------- * Setter for the "LSArchitecturePriority" attribute (optional) */ public void setLSArchitecturePriority(String s) { bundleProperties.setLSArchitecturePriority(s); } //------------------------------------------------------- /** * Setter for the "startonmainthread" attribute (optional) */ public void setStartonmainthread(boolean b) { bundleProperties.setStartOnMainThread(new Boolean(b)); } /** * Setter for the "startasagent" attribute (optional) */ public void setIsAgent( boolean b ) { bundleProperties.setLSUIElement( new Boolean( b ) ); } /** * Setter for the "verbose" attribute (optional) */ public void setVerbose(boolean verbose) { this.mVerbose = verbose; } public void setShowPlist(boolean showPlist) { this.mShowPlist = showPlist; } /** * Setter for the "buildnumber" attribute (optional) This key specifies the * exact build version of the bundle. This string is usually of the form * nn.n.nxnnn where n is a digit and x is a character from the set [abdf]. * The first number is the major version number of the bundle and can * contain one or two digits to represent a number in the range 0-99. The * second and third numbers are minor revision numbers and must be a single * numeric digit. The fourth set of digits is the specific build number for * the release. * * You may omit minor revision and build number information as appropriate. * You may also omit major and minor revision information and specify only a * build number. For example, valid version numbers include: 1.0.1, * 1.2.1b10, 1.2d200, d125, 101, and 1.0. * * The value of this key typically changes between builds and is displayed * in the Cocoa About panel in parenthesis. To specify the version * information of a released bundle, use the CFBundleShortVersionString key. */ public void setBuild(String s) { bundleProperties.setCFBundleVersion(s); } /** * Setter for the version attribute (optional). It is this property, not * CFBundleVersion, which should receive the `short' version string. See for * example * <http://developer.apple.com/documentation/MacOSX/Conceptual/BPRuntimeConfig/> */ public void setVersion(String s) { bundleProperties.setCFBundleShortVersionString(s); } public void setHelpBookFolder(String s) { bundleProperties.setCFBundleHelpBookFolder(s); } public void setHelpBookName(String s) { bundleProperties.setCFBundleHelpBookName(s); } /** * Setter for the "jars" attribute (required if no "jarfileset" is present) */ public void setJars(String s) { PatternSet patset = new PatternSet(); patset.setIncludes(s); String[] jarNames = patset.getIncludePatterns(getProject()); for (int i = 0; i < jarNames.length; i++) mJarAttrs.add(getProject().resolveFile(jarNames[i])); } /** * Setter for the "jar" attribute (required if no "jarfileset" is present) */ public void setJar(File s) { mJarAttrs.add(s); } /** * Setter for the "execs" attribute (optional) */ public void setExecs(String s) { PatternSet patset = new PatternSet(); patset.setIncludes(s); String[] execNames = patset.getIncludePatterns(getProject()); for (int i = 0; i < execNames.length; i++) { File f = new File(execNames[i]); mExecAttrs.add(f); } } /** * Setter for the "extraclasspath" attribute (optional) */ public void setExtraclasspath(String s) { if (s == null || s.trim().equals("")) return; PatternSet patset = new PatternSet(); patset.setIncludes(s); String[] cpNames = patset.getIncludePatterns(getProject()); for (int i = 0; i < cpNames.length; i++) { File f = new File(cpNames[i]); mExtraClassPathAttrs.add(f); } } /** * Set the 'chmod' executable. */ public void setChmod(String s) { log("The \"chmod\" attribute is deprecated, this task uses the ANT Chmod task internally now instead"); } /*************************************************************************** * Nested tasks - derived from FileList and FileSet **************************************************************************/ public void addJarfileset(FileSet fs) { mJarFileSets.add(fs); } public void addJarfilelist(FileList fl) { mJarFileLists.add(fl); } public void addExecfileset(FileSet fs) { mExecFileSets.add(fs); } public void addExecfilelist(FileList fl) { mExecFileLists.add(fl); } public void addResourcefileset(FileSet fs) { mResourceFileSets.add(fs); } public void addResourcefilelist(FileList fl) { mResourceFileLists.add(fl); } public void addJavafileset(FileSet fs) { mJavaFileSets.add(fs); } public void addJavafilelist(FileList fl) { mJavaFileLists.add(fl); } public void addExtraclasspathfileset(FileSet fs) { mExtraClassPathFileSets.add(fs); } public void addExtraclasspathfilelist(FileList fl) { mExtraClassPathFileLists.add(fl); } /*************************************************************************** * Nested tasks - new tasks with custom attributes **************************************************************************/ //new ins 08/05/2015 Tobias Bley / UltraMixer public void addConfiguredLSEnvironment(LSEnvironment lsEnvironment) throws BuildException { String name = lsEnvironment.getName(); String value = lsEnvironment.getValue(); if ((name == null) || (value == null)) throw new BuildException( "'<lsenvironment>' must have both 'name' and 'value' attibutes"); bundleProperties.addLSEnvironment(name, value); } public void addConfiguredJavaProperty(JavaProperty javaProperty) throws BuildException { String name = javaProperty.getName(); String value = javaProperty.getValue(); if ((name == null) || (value == null)) throw new BuildException( "'<javaproperty>' must have both 'name' and 'value' attibutes"); bundleProperties.addJavaProperty(name, value); } public void addConfiguredDocumentType(DocumentType documentType) throws BuildException { String name = documentType.getName(); String role = documentType.getRole(); List osTypes = documentType.getOSTypes(); List extensions = documentType.getExtensions(); List mimeTypes = documentType.getMimeTypes(); if ((name == null) || (role == null)) throw new BuildException( "'<documenttype>' must have both a 'name' and a 'role' attibute"); if ((osTypes.isEmpty()) && (extensions.isEmpty()) && (mimeTypes.isEmpty())) throw new BuildException( "'<documenttype>' of \"" + name + "\" must have 'osTypes' or 'extensions' or 'mimeTypes'"); bundleProperties.addDocumentType(documentType); } public void addConfiguredService(Service service) { //if (service.getPortName() == null) // throw new BuildException("\"<service>\" must have a \"portName\" attribute"); if (service.getMessage() == null) throw new BuildException("\"<service>\" must have a \"message\" attribute"); String menuItem = service.getMenuItem(); if (menuItem == null) throw new BuildException("\"<service>\" must have a \"menuItem\" attribute"); if (!menuItems.add(menuItem)) throw new BuildException("\"<service>\" \"menuItem\" value must be unique"); if (service.getSendTypes().isEmpty() && service.getReturnTypes().isEmpty()) throw new BuildException("\"<service>\" must have either a \"sendTypes\" attribute, a \"returnTypes\" attribute or both"); String keyEquivalent = service.getKeyEquivalent(); if ((keyEquivalent != null) && (1 != keyEquivalent.length())) throw new BuildException("\"<service>\" \"keyEquivalent\" must be one character if present"); String timeoutString = service.getTimeout(); if (timeoutString != null) { long timeout = -1; try { timeout = Long.parseLong(timeoutString); } catch (NumberFormatException nfe) { throw new BuildException("\"<service>\" \"timeout\" must be a positive integral number"); } if (timeout < 0) throw new BuildException("\"<service>\" \"timeout\" must not be negative"); } bundleProperties.addService(service); } public void addConfiguredHelpBook(HelpBook helpBook) { // Validity check on 'foldername' if (helpBook.getFolderName() == null) { if (bundleProperties.getCFBundleHelpBookFolder() == null) throw new BuildException("Either the '<helpbook>' attribute 'foldername' or the '<jarbundler>' attribute 'helpbookfolder' must be defined"); helpBook.setFolderName(bundleProperties.getCFBundleHelpBookFolder()); } // Validity check on 'title' if (helpBook.getName() == null) { if (bundleProperties.getCFBundleHelpBookName() == null) throw new BuildException("Either the '<helpbook>' attribute 'name' or the '<jarbundler>' attribute 'helpbookname' must be defined"); helpBook.setName(bundleProperties.getCFBundleHelpBookName()); } // Make sure some file were selected... List fileLists = helpBook.getFileLists(); List fileSets = helpBook.getFileSets(); if ( fileLists.isEmpty() && fileSets.isEmpty() ) throw new BuildException("The '<helpbook>' task must have either " + "'<fileset>' or '<filelist>' nested tags"); mHelpBooks.add(helpBook); } /*************************************************************************** * Execute the task **************************************************************************/ /** * The method executing the task */ public void execute() throws BuildException { // Delete any existing Application bundle directory structure bundleDir = new File(mRootDir, bundleProperties.getApplicationName() + ".app"); if (bundleDir.exists()) { Delete deleteTask = new Delete(); deleteTask.setProject(getProject()); deleteTask.setDir(bundleDir); deleteTask.execute(); } // Validate - look for required attributes // /////////////////////////////////////////// if (mRootDir == null) throw new BuildException("Required attribute \"dir\" is not set."); if (mJarAttrs.isEmpty() && mJarFileSets.isEmpty() && mJarFileLists.isEmpty()) throw new BuildException("Either the attribute \"jar\" must " + "be set, or one or more jarfilelists or " + "jarfilesets must be added."); if (!mJarAttrs.isEmpty() && (!mJarFileSets.isEmpty() || !mJarFileLists.isEmpty())) throw new BuildException( "Cannot set both the attribute " + "\"jars\" and use jar filesets/filelists. Use only one or the other."); if (bundleProperties.getApplicationName() == null) throw new BuildException("Required attribute \"name\" is not set."); if (bundleProperties.getMainClass() == null) throw new BuildException( "Required attribute \"mainclass\" is not set."); // ///////////////////////////////////////////////////////////////////////////////////// // Set up some Java properties // About Menu, deprecated under 1.4+ if (useOldPropertyNames()) bundleProperties.addJavaProperty(ABOUTMENU_KEY, bundleProperties .getCFBundleName()); // Anti Aliased Graphics, renamed in 1.4+ String antiAliasedProperty = useOldPropertyNames() ? "com.apple.macosx.AntiAliasedGraphicsOn" : "apple.awt.antialiasing"; if (mAntiAliasedGraphics != null) bundleProperties.addJavaProperty(antiAliasedProperty, mAntiAliasedGraphics.toString()); // Anti Aliased Text, renamed in 1.4+ String antiAliasedTextProperty = useOldPropertyNames() ? "com.apple.macosx.AntiAliasedTextOn" : "apple.awt.textantialiasing"; if (mAntiAliasedText != null) bundleProperties.addJavaProperty(antiAliasedTextProperty, mAntiAliasedText.toString()); // Live Resize, deprecated under 1.4+ if (useOldPropertyNames() && (mLiveResize != null)) bundleProperties.addJavaProperty( "com.apple.mrj.application.live-resize", mLiveResize .toString()); // Screen Menu Bar, renamed in 1.4+ String screenMenuBarProperty = useOldPropertyNames() ? "com.apple.macos.useScreenMenuBar" : "apple.laf.useScreenMenuBar"; if (mScreenMenuBar != null) bundleProperties.addJavaProperty(screenMenuBarProperty, mScreenMenuBar.toString()); // Growbox, added with 1.4+ if ((useOldPropertyNames() == false) && (mGrowbox != null)) bundleProperties.addJavaProperty("apple.awt.showGrowBox", mGrowbox .toString()); // Growbox Intrudes, deprecated under 1.4+ if (useOldPropertyNames() && (mGrowboxIntrudes != null)) bundleProperties.addJavaProperty( "com.apple.mrj.application.growbox.intrudes", mGrowboxIntrudes.toString()); if (!mRootDir.exists() || (mRootDir.exists() && !mRootDir.isDirectory())) throw new BuildException( "Destination directory specified by \"dir\" " + "attribute must already exist."); if (bundleDir.exists()) throw new BuildException("The directory/bundle \"" + bundleDir.getName() + "\" already exists, cannot continue."); // Status message log("Creating application bundle: " + bundleDir); if (!bundleDir.mkdir()) throw new BuildException("Unable to create bundle: " + bundleDir); // Make the Contents directory mContentsDir = new File(bundleDir, "Contents"); if (!mContentsDir.mkdir()) throw new BuildException("Unable to create directory " + mContentsDir); // Make the "MacOS" directory mMacOsDir = new File(mContentsDir, "MacOS"); if (!mMacOsDir.mkdir()) throw new BuildException("Unable to create directory " + mMacOsDir); // Make the Resources directory mResourcesDir = new File(mContentsDir, "Resources"); if (!mResourcesDir.mkdir()) throw new BuildException("Unable to create directory " + mResourcesDir); // Make the Resources/Java directory mJavaDir = new File(bundleProperties.getJavaVersion() < 1.7 ? mResourcesDir : mContentsDir, "Java"); if (!mJavaDir.mkdir()) throw new BuildException("Unable to create directory " + mJavaDir); // Copy icon file to resource dir. If no icon parameter // is supplied, the default icon will be used. if (mAppIcon != null) { try { File dest = new File(mResourcesDir, mAppIcon.getName()); if(mVerbose) log("Copying application icon file to \"" + bundlePath(dest) + "\""); mFileUtils.copyFile(mAppIcon, dest); } catch (IOException ex) { throw new BuildException("Cannot copy icon file: " + ex); } } // Copy document type icons, if any, to the resource dir try { Iterator itor = bundleProperties.getDocumentTypes().iterator(); while (itor.hasNext()) { DocumentType documentType = (DocumentType) itor.next(); File iconFile = documentType.getIconFile(); if (iconFile != null) { File dest = new File(mResourcesDir, iconFile.getName()); if(mVerbose) log("Copying document icon file to \"" + bundlePath(dest) + "\""); mFileUtils.copyFile(iconFile, dest); } } } catch (IOException ex) { throw new BuildException("Cannot copy document icon file: " + ex); } // Copy application jar(s) from the "jars" attribute (if any) processJarAttrs(); // Copy application jar(s) from the nested jarfileset element(s) processJarFileSets(); // Copy application jar(s) from the nested jarfilelist element(s) processJarFileLists(); // Copy executable(s) from the "execs" attribute (if any) processExecAttrs(); // Copy executable(s) from the nested execfileset element(s) processExecFileSets(); // Copy executable(s) from the nested execfilelist element(s) processExecFileLists(); // Copy resource(s) from the nested resourcefileset element(s) processResourceFileSets(); // Copy resource(s) from the nested javafileset element(s) processJavaFileSets(); // Copy resource(s) from the nested resourcefilelist element(s) processResourceFileLists(); // Copy resource(s) from the nested javafilelist element(s) processJavaFileLists(); // Add external classpath references from the extraclasspath attributes processExtraClassPathAttrs(); // Add external classpath references from the nested // extraclasspathfileset element(s) processExtraClassPathFileSets(); // Add external classpath references from the nested // extraclasspathfilelist attributes processExtraClassPathFileLists(); // Copy HelpBooks into place copyHelpBooks(); // Copy the JavaApplicationStub file from the Java system directory to // the MacOS directory copyApplicationStub(); // Create the Info.plist file writeInfoPlist(); // Create the PkgInfo file writePkgInfo(); // Done! } /*************************************************************************** * Private utility methods. **************************************************************************/ private void setExecutable(File f) { Chmod chmodTask = new Chmod(); chmodTask.setProject(getProject()); chmodTask.setFile(f); chmodTask.setPerm("ugo+rx"); if (mVerbose) log("Setting \"" + bundlePath(f) + "\" to executable"); chmodTask.execute(); } /** * Utility method to determine whether this app bundle is targeting a 1.3 or * 1.4 VM. The Mac OS X 1.3 VM uses different Java property names from the * 1.4 VM to hint at native Mac OS X look and feel options. For example, on * 1.3 the Java property to tell the VM to display Swing menu bars as screen * menus is "com.apple.macos.useScreenMenuBar". Under 1.4, it becomes * "apple.laf.useScreenMenuBar". Such is the price of progress, I suppose. * * Obviously, this logic may need refactoring in the future. */ private boolean useOldPropertyNames() { return (bundleProperties.getJavaVersion() <= 1.3); } private void processJarAttrs() throws BuildException { try { for (Iterator jarIter = mJarAttrs.iterator(); jarIter.hasNext();) { File src = (File) jarIter.next(); File dest = new File(mJavaDir, src.getName()); if (mVerbose) log("Copying JAR file to \"" + bundlePath(dest) + "\""); mFileUtils.copyFile(src, dest); bundleProperties.addToClassPath(dest.getName()); } } catch (IOException ex) { throw new BuildException("Cannot copy jar file: " + ex); } } private void processJarFileSets() throws BuildException { for (Iterator jarIter = mJarFileSets.iterator(); jarIter.hasNext();) { FileSet fs = (FileSet) jarIter.next(); Project p = fs.getProject(); File srcDir = fs.getDir(p); FileScanner ds = fs.getDirectoryScanner(p); fs.setupDirectoryScanner(ds, p); ds.scan(); String[] files = ds.getIncludedFiles(); try { for (int i = 0; i < files.length; i++) { String fileName = files[i]; File src = new File(srcDir, fileName); File dest = new File(mJavaDir, fileName); if (mVerbose) log("Copying JAR file to \"" + bundlePath(dest) + "\""); mFileUtils.copyFile(src, dest); bundleProperties.addToClassPath(fileName); } } catch (IOException ex) { throw new BuildException("Cannot copy jar file: " + ex); } } } private void processJarFileLists() throws BuildException { for (Iterator jarIter = mJarFileLists.iterator(); jarIter.hasNext();) { FileList fl = (FileList) jarIter.next(); Project p = fl.getProject(); File srcDir = fl.getDir(p); String[] files = fl.getFiles(p); try { for (int i = 0; i < files.length; i++) { String fileName = files[i]; File src = new File(srcDir, fileName); File dest = new File(mJavaDir, fileName); if (mVerbose) log("Copying JAR file to \"" + bundlePath(dest) + "\""); mFileUtils.copyFile(src, dest); bundleProperties.addToClassPath(fileName); } } catch (IOException ex) { throw new BuildException("Cannot copy jar file: " + ex); } } } private void processExtraClassPathAttrs() throws BuildException { for (Iterator jarIter = mExtraClassPathAttrs.iterator(); jarIter .hasNext();) { File src = (File) jarIter.next(); String path = src.getPath().replace(File.separatorChar, '/'); bundleProperties.addToExtraClassPath(path); } } private void processExtraClassPathFileSets() throws BuildException { for (Iterator jarIter = mExtraClassPathFileSets.iterator(); jarIter .hasNext();) { FileSet fs = (FileSet) jarIter.next(); Project p = fs.getProject(); File srcDir = fs.getDir(p); FileScanner ds = fs.getDirectoryScanner(p); fs.setupDirectoryScanner(ds, p); ds.scan(); String[] files = ds.getIncludedFiles(); for (int i = 0; i < files.length; i++) { File f = new File(srcDir, files[i]); String path = f.getPath().replace(File.separatorChar, '/'); bundleProperties.addToExtraClassPath(path); } } } private void processExtraClassPathFileLists() throws BuildException { for (Iterator jarIter = mExtraClassPathFileLists.iterator(); jarIter .hasNext();) { FileList fl = (FileList) jarIter.next(); Project p = fl.getProject(); File srcDir = fl.getDir(p); String[] files = fl.getFiles(p); for (int i = 0; i < files.length; i++) { File f = new File(srcDir, files[i]); String path = f.getPath().replace(File.separatorChar, '/'); bundleProperties.addToExtraClassPath(path); } } } private void processExecAttrs() throws BuildException { try { for (Iterator execIter = mExecAttrs.iterator(); execIter.hasNext();) { File src = (File) execIter.next(); File dest = new File(mMacOsDir, src.getName()); if (mVerbose) log("Copying exec file to \"" + bundlePath(dest) + "\""); mFileUtils.copyFile(src, dest); setExecutable(dest); } } catch (IOException ex) { throw new BuildException("Cannot copy exec file: " + ex); } } // Methods for copying FileSets into the application bundle /////////////////////////////// // Files for the Contents/MacOS directory private void processExecFileSets() { processCopyingFileSets(mExecFileSets, mMacOsDir, true); } // Files for the Contents/Resources directory private void processResourceFileSets() { processCopyingFileSets(mResourceFileSets, mResourcesDir, false); } // Files for the Contents/Resources/Java directory private void processJavaFileSets() { processCopyingFileSets(mJavaFileSets, mJavaDir, false); } private void processCopyingFileSets(List fileSets, File targetdir, boolean setExec) { for (Iterator execIter = fileSets.iterator(); execIter.hasNext();) { FileSet fs = (FileSet) execIter.next(); Project p = fs.getProject(); File srcDir = fs.getDir(p); FileScanner ds = fs.getDirectoryScanner(p); fs.setupDirectoryScanner(ds, p); ds.scan(); String[] files = ds.getIncludedFiles(); if (files.length == 0) { // this is probably an error -- warn about it System.err .println("WARNING: fileset for copying from directory " + srcDir + ": no files found"); } else { try { for (int i = 0; i < files.length; i++) { String fileName = files[i]; File src = new File(srcDir, fileName); File dest = new File(targetdir, fileName); if (mVerbose) log("Copying " + (setExec ? "exec" : "resource") + " file to \"" + bundlePath(dest) +"\""); mFileUtils.copyFile(src, dest); if (setExec) setExecutable(dest); } } catch (IOException ex) { throw new BuildException("Cannot copy file: " + ex); } } } } // Methods for copying FileLists into the application bundle ///////////////////////////// // Files for the Contents/MacOS directory private void processExecFileLists() throws BuildException { processCopyingFileLists(mExecFileLists, mMacOsDir, true); } // Files for the Contents/Resources directory private void processResourceFileLists() throws BuildException { processCopyingFileLists(mResourceFileLists, mResourcesDir, false); } // Files for the Contents/Resources/Java directory private void processJavaFileLists() throws BuildException { processCopyingFileLists(mJavaFileLists, mJavaDir, false); } private void processCopyingFileLists(List fileLists, File targetDir, boolean setExec) throws BuildException { for (Iterator execIter = fileLists.iterator(); execIter.hasNext();) { FileList fl = (FileList) execIter.next(); Project p = fl.getProject(); File srcDir = fl.getDir(p); String[] files = fl.getFiles(p); if (files.length == 0) { // this is probably an error -- warn about it System.err.println("WARNING: filelist for copying from directory " + srcDir + ": no files found"); } else { try { for (int i = 0; i < files.length; i++) { String fileName = files[i]; File src = new File(srcDir, fileName); File dest = new File(targetDir, fileName); if (mVerbose) log("Copying " + (setExec ? "exec" : "resource") + " file to \"" + bundlePath(dest) +"\""); mFileUtils.copyFile(src, dest); if (setExec) setExecutable(dest); } } catch (IOException ex) { throw new BuildException("Cannot copy jar file: " + ex); } } } } private void copyHelpBooks() { for (Iterator itor = mHelpBooks.iterator(); itor.hasNext();) { HelpBook helpBook = (HelpBook)itor.next(); String folderName = helpBook.getFolderName(); String name = helpBook.getName(); String locale = helpBook.getLocale(); List fileLists = helpBook.getFileLists(); List fileSets = helpBook.getFileSets(); File helpBookDir = null; if (locale == null) { // Set the Bundle entries for a nonlocalized Help Book if (folderName != null) bundleProperties.setCFBundleHelpBookFolder(folderName); if (name != null) bundleProperties.setCFBundleHelpBookName(name); // The non-localized Help Book is top level "/Resources" helpBookDir = new File(mResourcesDir, folderName); helpBookDir.mkdir(); if(mVerbose) log("Creating Help Book at \"" + bundlePath(helpBookDir) + "\""); } else { // The localized Help Book is "/Resources/locale.lproj" File lproj = new File(mResourcesDir, locale + ".lproj"); lproj.mkdir(); helpBookDir = new File(lproj, folderName); helpBookDir.mkdir(); if(mVerbose) log("Creating Help Book for \"" + locale + "\" at \"" + bundlePath(helpBookDir) + "\""); // Create a local file to override the Bundle settings File infoPList = new File(lproj, "InfoPlist.strings"); PrintWriter writer = null; try { writer = new PrintWriter(new FileWriter(infoPList)); writer.println("CFBundleHelpBookFolder = \"" + folderName + "\";"); writer.println("CFBundleHelpBookName = \"" + name + "\";"); writer.println("CFBundleName = \"" + bundleProperties.getCFBundleName() + "\";"); } catch (IOException ioe) { throw new BuildException("IOException in writing Help Book locale: " + locale); } finally { mFileUtils.close(writer); } } // Write the Help Book source files into the bundle processCopyingFileSets(fileSets, helpBookDir, false); processCopyingFileLists(fileLists, helpBookDir, false); } } // Copy the application stub into the bundle // ///////////////////////////////////////////// private void copyApplicationStub() throws BuildException { File newStubFile = new File(mMacOsDir, bundleProperties.getCFBundleExecutable()); if (mVerbose) log("Copying Java application stub to \"" + bundlePath(newStubFile) + "\""); try { mFileUtils.copyFile(mStubFile, newStubFile); } catch (IOException ex) { throw new BuildException("Cannot copy Java Application Stub: " + ex); } // Set the permissions on the stub file to executable setExecutable(newStubFile); } private void writeInfoPlist() throws BuildException { PropertyListWriter listWriter = new PropertyListWriter(bundleProperties); File infoPlist = new File(mContentsDir, "Info.plist"); listWriter.writeFile(infoPlist); if (mVerbose) log("Creating \"" + bundlePath(infoPlist) + "\" file"); if (mShowPlist) { try { BufferedReader in = new BufferedReader(new FileReader(infoPlist)); String str; while ((str = in.readLine()) != null) log(str); in.close(); } catch (IOException e) { throw new BuildException(e); } } } // // Write the PkgInfo file into the application bundle // private void writePkgInfo() throws BuildException { File pkgInfo = new File(mContentsDir, "PkgInfo"); PrintWriter writer = null; try { writer = new PrintWriter(new BufferedWriter(new FileWriter(pkgInfo))); writer.print(bundleProperties.getCFBundlePackageType()); writer.println(bundleProperties.getCFBundleSignature()); writer.flush(); } catch (IOException ex) { throw new BuildException("Cannot create PkgInfo file: " + ex); } finally { mFileUtils.close(writer); } } private String bundlePath(File bundleFile) { String rootPath = bundleDir.getAbsolutePath(); String thisPath = bundleFile.getAbsolutePath(); return thisPath.substring(rootPath.length()); } public void setSUFeedURL(String url) { this.bundleProperties.setSUFeedURL(url); } public void setSUPublicDSAKeyFile(String file) { this.bundleProperties.setSUPublicDSAKeyFile(file); } public void setLSApplicationCategoryType(String type) { bundleProperties.setLSApplicationCategoryType(type); } }