/*
* Copyright 2012, Oracle and/or its affiliates. All rights reserved.
*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.oracle.appbundler;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.types.resources.FileResource;
/**
* App bundler Ant task.
*/
public class AppBundlerTask extends Task {
// Output folder for generated bundle
private File outputDirectory = null;
// General bundle properties
private String name = null;
private String displayName = null;
private String identifier = null;
private File iconFile = null;
private String executableName = EXECUTABLE_NAME;
private String shortVersion = null; //"1.0";
private String version = null; //"1.0";
private String signature = "????";
private String copyright = null; //"";
private String getInfo = null;
private String privileged = null;
private String workingDirectory = null;
private String applicationCategory = null;
private boolean highResolutionCapable = true;
// Oracle Java 8 requires 10.8.3 or later, so require it here.
private String minimumSystem = "10.8.3";
// By default, don't embed Java FX.
private boolean javafx = false;
// JVM info properties
private String mainClassName = null;
private FileSet runtime = null;
private ArrayList<FileSet> classPath = new ArrayList<>();
private ArrayList<FileSet> libraryPath = new ArrayList<>();
private ArrayList<String> options = new ArrayList<>();
private ArrayList<String> arguments = new ArrayList<>();
private ArrayList<String> architectures = new ArrayList<>();
private ArrayList<BundleDocument> bundleDocuments = new ArrayList<>();
private Reference classPathRef;
private static final String EXECUTABLE_NAME = "JavaAppLauncher";
private static final String DEFAULT_ICON_NAME = "GenericApp.icns";
private static final String OS_TYPE_CODE = "APPL";
private static final int BUFFER_SIZE = 2048;
public void setOutputDirectory(File outputDirectory) {
this.outputDirectory = outputDirectory;
}
public void setName(String name) {
this.name = name;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
public void setIcon(File icon) {
this.iconFile = icon;
}
public void setExecutableName(String executable) {
this.executableName = executable;
}
public void setShortVersion(String shortVersion) {
this.shortVersion = shortVersion;
}
public void setVersion(String version) {
this.version = version;
}
public void setSignature(String signature) {
this.signature = signature;
}
public void setCopyright(String copyright) {
this.copyright = copyright;
}
public void setGetInfo(String getInfo) {
this.getInfo = getInfo;
}
public void setPrivileged(String privileged) {
this.privileged = privileged;
}
public void setWorkingDirectory(String workingDirectory) {
this.workingDirectory = workingDirectory;
}
public void setApplicationCategory(String applicationCategory) {
this.applicationCategory = applicationCategory;
}
public void setMinimumSystem(String minimumSystem) {
this.minimumSystem = minimumSystem;
}
public void setHighResolutionCapable(boolean highResolutionCapable) {
this.highResolutionCapable = highResolutionCapable;
}
public void setJavaFX(boolean javafx) {
this.javafx = javafx;
}
public void setMainClassName(String mainClassName) {
this.mainClassName = mainClassName;
}
public void addConfiguredRuntime(FileSet runtime) throws BuildException {
if (this.runtime != null) {
throw new BuildException("Runtime already specified.");
}
this.runtime = runtime;
runtime.appendIncludes(new String[] {
"jre/",
});
runtime.appendExcludes(new String[] {
"bin/",
// original version, removed entire bin folder
//"jre/bin/",
// remove everything except 'java'
// also keep 'keytool' (needed by Android)
"jre/bin/orbd",
"jre/bin/pack200",
"jre/bin/policytool",
"jre/bin/rmid",
"jre/bin/rmiregistry",
"jre/bin/servertool",
"jre/bin/tnameserv",
"jre/bin/unpack200",
"jre/lib/deploy/",
"jre/lib/deploy.jar",
"jre/lib/javaws.jar",
"jre/lib/libdeploy.dylib",
"jre/lib/libnpjp2.dylib",
"jre/lib/plugin.jar",
"jre/lib/security/javaws.policy"
});
if (!javafx) {
// http://www.oracle.com/technetwork/java/javase/jdk-7-readme-429198.html
runtime.appendExcludes(new String[] {
"jre/THIRDPARTYLICENSEREADME-JAVAFX.txt",
"jre/lib/javafx.properties",
"jre/lib/jfxrt.jar",
"jre/lib/security/javafx.policy",
"jre/lib/fxplugins.dylib",
"jre/lib/libdecora-sse.dylib",
"jre/lib/libglass.dylib",
"jre/lib/libglib-2.0.0.dylib",
"jre/lib/libgstplugins-lite.dylib",
"jre/lib/libgstreamer-lite.dylib",
"jre/lib/libjavafx-font.dylib",
"jre/lib/libjavafx-iio.dylib",
"jre/lib/libjfxmedia.dylib",
"jre/lib/libjfxwebkit.dylib",
"jre/lib/libprism-es2.dylib"
});
}
}
public void setClasspathRef(Reference ref) {
this.classPathRef = ref;
}
public void addConfiguredClassPath(FileSet classPath) {
this.classPath.add(classPath);
}
public void addConfiguredLibraryPath(FileSet libraryPath) {
this.libraryPath.add(libraryPath);
}
public void addConfiguredBundleDocument(BundleDocument document) {
this.bundleDocuments.add(document);
}
public void addConfiguredOption(Option option) throws BuildException {
String value = option.getValue();
if (value == null) {
throw new BuildException("Value is required.");
}
options.add(value);
}
public void addConfiguredArgument(Argument argument) throws BuildException {
String value = argument.getValue();
if (value == null) {
throw new BuildException("Value is required.");
}
arguments.add(value);
}
public void addConfiguredArch(Architecture architecture) throws BuildException {
String name = architecture.getName();
if (name == null) {
throw new BuildException("Name is required.");
}
architectures.add(name);
}
@Override
public void execute() throws BuildException {
// Validate required properties
if (outputDirectory == null) {
throw new IllegalStateException("Output directory is required.");
}
if (!outputDirectory.exists()) {
throw new IllegalStateException("Output directory does not exist.");
}
if (!outputDirectory.isDirectory()) {
throw new IllegalStateException("Invalid output directory.");
}
if (name == null) {
throw new IllegalStateException("Name is required.");
}
if (displayName == null) {
throw new IllegalStateException("Display name is required.");
}
if (identifier == null) {
throw new IllegalStateException("Identifier is required.");
}
if (iconFile != null) {
if (!iconFile.exists()) {
throw new IllegalStateException("Icon does not exist.");
}
if (iconFile.isDirectory()) {
throw new IllegalStateException("Invalid icon.");
}
}
if (shortVersion == null) {
throw new IllegalStateException("Short version is required.");
}
if (signature == null) {
throw new IllegalStateException("Signature is required.");
}
if (signature.length() != 4) {
throw new IllegalStateException("Invalid signature.");
}
if (copyright == null) {
throw new IllegalStateException("Copyright is required.");
}
if (mainClassName == null) {
throw new IllegalStateException("Main class name is required.");
}
// Create the app bundle
try {
System.out.println("Creating app bundle: " + name);
// Create directory structure
File rootDirectory = new File(outputDirectory, name + ".app");
delete(rootDirectory);
rootDirectory.mkdir();
File contentsDirectory = new File(rootDirectory, "Contents");
contentsDirectory.mkdir();
File macOSDirectory = new File(contentsDirectory, "MacOS");
macOSDirectory.mkdir();
File javaDirectory = new File(contentsDirectory, "Java");
javaDirectory.mkdir();
File plugInsDirectory = new File(contentsDirectory, "PlugIns");
plugInsDirectory.mkdir();
File resourcesDirectory = new File(contentsDirectory, "Resources");
resourcesDirectory.mkdir();
// // Move back to Contents/Resources/Java instead of Contents/Java [fry]
// File javaDirectory = new File(resourcesDirectory, "Java");
// javaDirectory.mkdir();
// Generate Info.plist
File infoPlistFile = new File(contentsDirectory, "Info.plist");
infoPlistFile.createNewFile();
writeInfoPlist(infoPlistFile);
// Generate PkgInfo
File pkgInfoFile = new File(contentsDirectory, "PkgInfo");
pkgInfoFile.createNewFile();
writePkgInfo(pkgInfoFile);
// Copy executable to MacOS folder
File executableFile = new File(macOSDirectory, executableName);
copy(getClass().getResource(EXECUTABLE_NAME), executableFile);
executableFile.setExecutable(true, false);
// Copy localized resources to Resources folder
copyResources(resourcesDirectory);
// Copy runtime to PlugIns folder
copyRuntime(plugInsDirectory);
// Copy class path entries to Java folder
copyClassPathEntries(javaDirectory);
// Copy class path ref entries to Java folder
copyClassPathRefEntries(javaDirectory);
// Copy library path entries to MacOS folder
copyLibraryPathEntries(macOSDirectory);
// Copy icon to Resources folder
copyIcon(resourcesDirectory);
// Copy the bundle/document icons as well
copyBundleIcons(resourcesDirectory);
} catch (IOException exception) {
throw new BuildException(exception);
}
}
private void copyResources(File resourcesDirectory) throws IOException {
// Unzip res.zip into resources directory
InputStream inputStream = getClass().getResourceAsStream("res.zip");
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
try {
ZipEntry zipEntry = zipInputStream.getNextEntry();
while (zipEntry != null) {
File file = new File(resourcesDirectory, zipEntry.getName());
if (zipEntry.isDirectory()) {
file.mkdir();
} else {
OutputStream outputStream =
new BufferedOutputStream(new FileOutputStream(file), BUFFER_SIZE);
try {
int b = zipInputStream.read();
while (b != -1) {
outputStream.write(b);
b = zipInputStream.read();
}
outputStream.flush();
} finally {
outputStream.close();
}
}
zipEntry = zipInputStream.getNextEntry();
}
} finally {
zipInputStream.close();
}
}
private void copyRuntime(File plugInsDirectory) throws IOException {
if (runtime != null) {
File runtimeHomeDirectory = runtime.getDir();
File runtimeContentsDirectory = runtimeHomeDirectory.getParentFile();
File runtimeDirectory = runtimeContentsDirectory.getParentFile();
// Create root plug-in directory
File pluginDirectory = new File(plugInsDirectory, runtimeDirectory.getName());
pluginDirectory.mkdir();
// Create Contents directory
File pluginContentsDirectory = new File(pluginDirectory, runtimeContentsDirectory.getName());
pluginContentsDirectory.mkdir();
// Copy MacOS directory
File runtimeMacOSDirectory = new File(runtimeContentsDirectory, "MacOS");
copy(runtimeMacOSDirectory, new File(pluginContentsDirectory, runtimeMacOSDirectory.getName()));
// Copy Info.plist file
File runtimeInfoPlistFile = new File(runtimeContentsDirectory, "Info.plist");
copy(runtimeInfoPlistFile, new File(pluginContentsDirectory, runtimeInfoPlistFile.getName()));
// Copy included contents of Home directory
File pluginHomeDirectory = new File(pluginContentsDirectory, runtimeHomeDirectory.getName());
DirectoryScanner directoryScanner = runtime.getDirectoryScanner(getProject());
String[] includedFiles = directoryScanner.getIncludedFiles();
for (String includedFile : includedFiles) {
//for (int i = 0; i < includedFiles.length; i++) {
//String includedFile = includedFiles[i];
File source = new File(runtimeHomeDirectory, includedFile);
File destination = new File(pluginHomeDirectory, includedFile);
copy(source, destination);
}
}
}
private void copyClassPathRefEntries(File javaDirectory) throws IOException {
if (classPathRef != null) {
org.apache.tools.ant.types.Path classpath =
(org.apache.tools.ant.types.Path) classPathRef.getReferencedObject(getProject());
Iterator<?> iter = classpath.iterator();
while (iter.hasNext()) {
FileResource resource = (FileResource) iter.next();
File source = resource.getFile();
File destination = new File(javaDirectory, source.getName());
copy(source, destination);
}
}
}
private void copyClassPathEntries(File javaDirectory) throws IOException {
for (FileSet fileSet : classPath) {
File classPathDirectory = fileSet.getDir();
DirectoryScanner directoryScanner = fileSet.getDirectoryScanner(getProject());
String[] includedFiles = directoryScanner.getIncludedFiles();
for (String includedFile : includedFiles) {
File source = new File(classPathDirectory, includedFile);
File destination = new File(javaDirectory, new File(includedFile).getName());
copy(source, destination);
}
}
}
private void copyLibraryPathEntries(File macOSDirectory) throws IOException {
for (FileSet fileSet : libraryPath) {
File libraryPathDirectory = fileSet.getDir();
DirectoryScanner directoryScanner = fileSet.getDirectoryScanner(getProject());
String[] includedFiles = directoryScanner.getIncludedFiles();
for (String includedFile : includedFiles) {
File source = new File(libraryPathDirectory, includedFile);
File destination = new File(macOSDirectory, new File(includedFile).getName());
copy(source, destination);
}
}
}
private void copyIcon(File resourcesDirectory) throws IOException {
if (iconFile == null) {
copy(getClass().getResource(DEFAULT_ICON_NAME),
new File(resourcesDirectory, DEFAULT_ICON_NAME));
} else {
copy(iconFile, new File(resourcesDirectory, iconFile.getName()));
}
}
private void copyBundleIcons(File resourcesDirectory) throws IOException {
for (BundleDocument bundleDocument : bundleDocuments) {
if (bundleDocument.hasIcon()) {
File iconFile = bundleDocument.getIconFile();
copy(iconFile, new File(resourcesDirectory, iconFile.getName()));
}
}
}
private void writeInfoPlist(File file) throws IOException {
FileOutputStream output = new FileOutputStream(file);
PropertyLister plist = new PropertyLister(output);
// Get started, write all necessary header info and open plist element
plist.writeStartDocument();
// Begin root dictionary
plist.writeStartDictElement();
// Write bundle properties
plist.writeProperty("CFBundleDevelopmentRegion", "English");
plist.writeProperty("CFBundleExecutable", executableName);
plist.writeProperty("CFBundleIconFile", (iconFile == null) ? DEFAULT_ICON_NAME : iconFile.getName());
plist.writeProperty("CFBundleIdentifier", identifier);
plist.writeProperty("CFBundleDisplayName", displayName);
plist.writeProperty("CFBundleInfoDictionaryVersion", "6.0");
plist.writeProperty("CFBundleName", name);
plist.writeProperty("CFBundlePackageType", OS_TYPE_CODE);
plist.writeProperty("CFBundleShortVersionString", shortVersion);
plist.writeProperty("CFBundleVersion", version);
plist.writeProperty("CFBundleSignature", signature);
plist.writeProperty("NSHumanReadableCopyright", copyright);
if (getInfo != null) {
plist.writeProperty("CFBundleGetInfoString", getInfo);
}
if (applicationCategory != null) {
plist.writeProperty("LSApplicationCategoryType", applicationCategory);
}
if (minimumSystem != null) {
plist.writeProperty("LSMinimumSystemVersion", minimumSystem);
}
if (highResolutionCapable) {
plist.writeKey("NSHighResolutionCapable");
plist.writeBoolean(true);
}
if (runtime != null) {
plist.writeProperty("JVMRuntime", runtime.getDir().getParentFile().getParentFile().getName());
}
if (privileged != null) {
plist.writeProperty("JVMRunPrivileged", privileged);
}
if (workingDirectory != null) {
plist.writeProperty("WorkingDirectory", workingDirectory);
}
// Write main class name
plist.writeProperty("JVMMainClassName", mainClassName);
// Write CFBundleDocument entries
plist.writeKey("CFBundleDocumentTypes");
plist.writeStartArrayElement();
for (BundleDocument bundleDocument: bundleDocuments) {
plist.writeStartDictElement();
plist.writeKey("CFBundleTypeExtensions");
plist.writeStartArrayElement();
for (String extension : bundleDocument.getExtensions()) {
plist.writeString(extension);
}
plist.writeEndElement();
if (bundleDocument.hasIcon()) {
plist.writeKey("CFBundleTypeIconFile");
plist.writeString(bundleDocument.getIconName());
}
plist.writeKey("CFBundleTypeName");
plist.writeString(bundleDocument.getName());
plist.writeKey("CFBundleTypeRole");
plist.writeString(bundleDocument.getRole());
plist.writeKey("LSTypeIsPackage");
plist.writeBoolean(bundleDocument.isPackage());
plist.writeEndElement();
}
plist.writeEndElement();
// Write architectures
plist.writeKey("LSArchitecturePriority");
plist.writeStartArrayElement();
for (String architecture : architectures) {
plist.writeString(architecture);
}
plist.writeEndElement();
// Write Environment
plist.writeKey("LSEnvironment");
plist.writeStartDictElement();
plist.writeKey("LC_CTYPE");
plist.writeString("UTF-8");
plist.writeEndElement();
// Write options
plist.writeKey("JVMOptions");
plist.writeStartArrayElement();
for (String option : options) {
plist.writeString(option);
}
plist.writeEndElement();
// Write arguments
plist.writeKey("JVMArguments");
plist.writeStartArrayElement();
for (String argument : arguments) {
plist.writeString(argument);
}
plist.writeEndElement();
// End root dictionary
plist.writeEndElement();
// Close out the plist
plist.writeEndDocument();
}
private void writePkgInfo(File file) throws IOException {
Writer out = new BufferedWriter(new FileWriter(file));
try {
out.write(OS_TYPE_CODE + signature);
out.flush();
} finally {
out.close();
}
}
private static void delete(File file) throws IOException {
Path filePath = file.toPath();
if (Files.exists(filePath, LinkOption.NOFOLLOW_LINKS)) {
if (Files.isDirectory(filePath, LinkOption.NOFOLLOW_LINKS)) {
File[] files = file.listFiles();
for (int i = 0; i < files.length; i++) {
delete(files[i]);
}
}
Files.delete(filePath);
}
}
private static void copy(URL location, File file) throws IOException {
try (InputStream in = location.openStream()) {
Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
}
private static void copy(File source, File destination) throws IOException {
Path sourcePath = source.toPath();
Path destinationPath = destination.toPath();
destination.getParentFile().mkdirs();
Files.copy(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING, LinkOption.NOFOLLOW_LINKS);
if (Files.isDirectory(sourcePath, LinkOption.NOFOLLOW_LINKS)) {
String[] files = source.list();
for (int i = 0; i < files.length; i++) {
String file = files[i];
copy(new File(source, file), new File(destination, file));
}
}
}
}