Статьи

Установка приложений на платформе NetBeans в Mac OS X с помощью Drag-n-Drop

Последние версии среды IDE NetBeans поставляются с меню «Установщики сборки», которое делает доступным для вашей платформы приложения те же установщики, которые используются в среде IDE. Это очень хорошая функция, хотя в Mac OS X вы можете создать приложение, которое можно просто установить, перетащив его в папку «Приложения». Mac OS X позволяет эту функцию с помощью «комплектов приложений».

Вчера я наконец-то портировал некоторый код из старой версии blueMarine Ant и Mavenized, чтобы его можно было легко использовать везде. Во-первых, есть плагин для приложений OS X Maven, но кажется, что он заброшен три года назад, и ему нужны некоторые специальные инструменты Mac OS X из пакета Developer. Не хорошо — например, я хочу все, включая установщик Mac OS X, подготовленный моим Hudson, работающим на Linux. Поэтому я предпочитаю использовать превосходный JarBundler, который работает на 100% Java.

К сожалению, похоже, что для JarBundler нет плагина Maven, только Ant Task. На помощь приходит maven-antrun-plugin, и это рабочая конфигурация:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
        <execution>
            <id>create-app-bundle</id>
            <phase>pre-integration-test</phase>
            <goals>
                <goal>run</goal>
            </goals>
            <configuration>
                <target>
                    <taskdef name="jarbundler"
                             classname="net.sourceforge.jarbundler.JarBundler"
                             classpath="src/main/app-resources/jarbundler-${tft.jarbundler.version}.jar" />

                    <mkdir dir="${project.build.directory}/appbundle"/>
                    <unjar src="${project.build.directory}/${project.build.finalName}.zip"
                           dest="${project.build.directory}/appbundle"/>

                    <jarbundler dir="${project.build.directory}"
                                jar="${project.build.directory}/appbundle/${netbeans.cluster}/platform/lib/boot.jar"
                                mainclass="dummy"
                                name="${tft.appbundle.name}"
                                jvmversion="${tft.javac.target}+"
                                showplist="false"
                                build="${project.version}"
                                bundleId="${project.groupId}"
                                icon="src/main/app-resources/${netbeans.cluster}.icns"
                                shortname="${tft.appbundle.name}"
                                signature="${tft.appbundle.signature}"
                                stubfile="src/main/app-resources/${netbeans.cluster}"
                                version="${project.version}">
                        <javafileset dir="src/main/app-resources">
                            <include name="**/*.icns" />
                        </javafileset>
                        <javafileset dir="${project.build.directory}/appbundle">
                            <include name="**/*" />
                        </javafileset>
                    </jarbundler>

                    <chmod file="${project.build.directory}/${tft.appbundle.name}.app/Contents/Resources/Java/${netbeans.cluster}/bin/${netbeans.cluster}"
                           perm="ugo+rx"/>

                    <exec dir="${project.build.directory}" os="Mac OS X" executable="hdiutil">
                        <arg value="create"/>
                        <arg value="-noanyowners"/>
                        <arg value="-imagekey"/>
                        <arg value="zlib-level=1"/>
                        <arg value="-srcfolder"/>
                        <arg value="${tft.appbundle.name}.app"/>
                        <arg value="${tft.appbundle.name}.dmg"/>
                    </exec>

                    <exec dir="${project.build.directory}" os="Linux" executable="mkisofs">
                        <arg value="-V"/>
                        <arg value="${tft.appbundle.name}"/>
                        <arg value="-U"/>
                        <arg value="-f"/>
                        <arg value="-D"/>
                        <arg value="-l"/>
                        <arg value="-L"/>
                        <arg value="-allow-multidot"/>
                        <arg value="-max-iso9660-filenames"/>
                        <arg value="-relaxed-filenames"/>
                        <arg value="-no-iso-translate"/>
                        <arg value="-r"/>
                        <arg value="-o"/>
                        <arg value="${tft.appbundle.name}.dmg"/>
                        <arg value="-root"/>
                        <arg value="${tft.appbundle.name}.app"/>
                        <arg value="${tft.appbundle.name}.app"/>
                    </exec>

                    <gzip src="${project.build.directory}/${tft.appbundle.name}.dmg"
                          destfile="${project.build.directory}/${tft.appbundle.name}.dmg.gz"/>
                </target>
            </configuration>
        </execution>
    </executions>
</plugin>

 

In a Maven NetBeans Platform application you have to configure this section in the «Application» module (the one with type nbm-application). It’s bound to the pre-integration-test phase since it’s the first one available right after the package phase: in fact, it relies on the .zip with all the distribution files to have been already prepared. It first extracts them to a temporary directory, then calls the JarBundler task which lays out them in the proper way for a Mac OS X application bundle. 

There are a couple of tricks. JarBundler by default prepares a Java launcher with a full classpath and the main class; on the other hand, the start of a NetBeans Platform application is more complex, as a shell script (which is auto-generated by the build) first inspects the installation, searching for NetBeans Platform clusters, then prepares the proper command line for Java. So we have to do the following things:

  1. Set a dummy parameter for ‘mainclass’ (JarBundler wants it)
  2. Set a dummy parameter for ‘jar’ (JarBundler wants it); since JarBundler checks that a file exists, the ‘boot.jar’ from the NetBeans Platform distribution is given.
  3. Set a fake stubfile (the stubfile usually is a small Mac OS X binary executable which parses the configuration of the application bundle, reads the JVM configuration, such as the minimum Java version, then finds a proper JVM installed in Mac OS X). My fake stubfile is just a shell script that finds and executes the launcher auto-generated by the build. For instance, for an application called ‘solidblue’ it’s something such as:
    #!/bin/sh
    
    dir=`dirname "$0"`
    
    exec $dir/../Resources/Java/solidblue/bin/solidblue

A second problem is how to pack the generated application bundle. It could be zipped, but Mac OS X users expect a .dmg file, which contains a disk image. On Mac OS X the hdiutil command can generate it, but there’s no hdiutil outside Mac OS X. Fortunately, on Linux systems there’s a mkisofs that can do the job. It has been originally designed for creating ISO 9660 (CD-ROM) images to be burned, but it can serve our scope. It just needs a bunch of arguments that make sure that the resulting artifact is mountable by Mac OS X and does not truncate long file names. hdiutil and mkisofs are conditionally executed by the Ant task, so the proper tool is picked in function of the current operating system.

A final trick is needed because, unfortunately, JarBundler is not available as a Maven artifact on a public repository. The solution I picked is to commit the jar file with sources and have it referenced by path by <taskdef>. Fortunately is a very small .jar file. So, the extra sources file that you need are:

[Mistral-MacOSX:SolidBlue/solidblue-src/application] fritz% ls -al src/main/app-resources/
total 1248
drwxrwx---  5 fritz  staff     170 Jan  4 01:49 .
drwxrwx---  3 fritz  staff     102 Oct 31 10:21 ..
-rw-r--r--@ 1 fritz  staff   18509 Dec 27  2010 jarbundler-2.2.0.jar
-rwxrwx--x  1 fritz  staff      84 Jan  4 01:49 solidblue
-rw-r--r--@ 1 fritz  staff  306657 Jan  4 01:57 solidblue.icns

As you can guess, if you put a .icns file (the Mac OS X standard for containing application icons), it will be picked and used, both for the Finder and for the application icon seen when you press cmd-tab to browse the currently running applications). Let’s prepare NetBeans Platform applications that are first-class citizen in Mac OS X and don’t scream immediately «Hey, I’ve been made with Java and I miss some o.s. integration stuff»!

As a final word, this is some stuff that needs to be reused. Early this year I published a trick that allows to mimic composition with Maven POMs, that is you can put a lot of stuff into your super pom and then activate it on demand (the various sections are wrapped in separate profiles that can be activated by the presence of a well known file). It’s what I’ve done for the chunk of POM configuration shown above, that is part of my own superpom and can be activated by putting an empty file named src/config/activate-netbeans-platform-appbundle-profile in your module. You can see two working examples of this setup in my projects SolidBlue (hg clone http://bitbucket.org/tidalwave/solidblue-src, tag 1.0-ALPHA-4) and blueArgyle (hg clone http://bitbucket.org/tidalwave/blueargyle-src, tag 1.0-ALPHA-3).