/* * 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.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; 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 javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; 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; /** * 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 icon = null; private String shortVersion = "1.0"; private String signature = "????"; private String copyright = ""; // JVM info properties private File runtime = null; private String mainClassName = null; private ArrayList<File> classPath = new ArrayList<>(); private ArrayList<File> nativeLibraries = new ArrayList<>(); private ArrayList<String> options = new ArrayList<>(); private ArrayList<String> arguments = new ArrayList<>(); public static final String EXECUTABLE_NAME = "JavaAppLauncher"; public static final String DEFAULT_ICON_NAME = "GenericApp.icns"; public static final String OS_TYPE_CODE = "APPL"; public static final String CLASS_EXTENSION = ".class"; public static final String PLIST_DTD = "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"; public static final String PLIST_TAG = "plist"; public static final String PLIST_VERSION_ATTRIBUTE = "version"; public static final String DICT_TAG = "dict"; public static final String KEY_TAG = "key"; public static final String ARRAY_TAG = "array"; public static final String STRING_TAG = "string"; public static final int BUFFER_SIZE = 1024; 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.icon = icon; } public void setShortVersion(String shortVersion) { this.shortVersion = shortVersion; } public void setSignature(String signature) { this.signature = signature; } public void setCopyright(String copyright) { this.copyright = copyright; } public File getRuntime() { return runtime; } public void setRuntime(File runtime) { this.runtime = runtime; } public void setMainClassName(String mainClassName) { this.mainClassName = mainClassName; } public void addConfiguredClassPath(FileSet classPath) { File parent = classPath.getDir(); DirectoryScanner directoryScanner = classPath.getDirectoryScanner(getProject()); String[] includedFiles = directoryScanner.getIncludedFiles(); for (int i = 0; i < includedFiles.length; i++) { this.classPath.add(new File(parent, includedFiles[i])); } } public void addNativeLibrary(File nativeLibrary) throws BuildException { if (nativeLibrary.isDirectory()) { throw new BuildException("Native library cannot be a directory."); } nativeLibraries.add(nativeLibrary); } 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); } @Override public void execute() throws BuildException { // Validate required properties if (outputDirectory == null) { throw new IllegalStateException("Destination directory is required."); } if (!outputDirectory.exists()) { throw new IllegalStateException("Destination directory does not exist."); } if (!outputDirectory.isDirectory()) { throw new IllegalStateException("Invalid destination 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 (icon != null) { if (!icon.exists()) { throw new IllegalStateException("Icon does not exist."); } if (icon.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 (runtime != null) { if (!runtime.exists()) { throw new IllegalStateException("Runtime does not exist."); } if (!runtime.isDirectory()) { throw new IllegalStateException("Invalid runtime."); } } if (mainClassName == null) { throw new IllegalStateException("Main class name is required."); } if (classPath.isEmpty()) { throw new IllegalStateException("Class path is required."); } // Create directory structure try { System.out.println("Creating app bundle: " + name); 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, "JavaVM"); javaDirectory.mkdir(); File classesDirectory = new File(javaDirectory, "Classes"); classesDirectory.mkdir(); File plugInsDirectory = new File(contentsDirectory, "PlugIns"); plugInsDirectory.mkdir(); File resourcesDirectory = new File(contentsDirectory, "Resources"); resourcesDirectory.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, EXECUTABLE_NAME); copy(getClass().getResource(EXECUTABLE_NAME), executableFile); executableFile.setExecutable(true); // Copy runtime to PlugIns folder (if specified) if (runtime != null) { copy(runtime, new File(plugInsDirectory, runtime.getName())); } // Copy class path entries to Java folder for (File entry : classPath) { String name = entry.getName(); if (entry.isDirectory() || name.endsWith(CLASS_EXTENSION)) { copy(entry, new File(classesDirectory, name)); } else { copy(entry, new File(javaDirectory, name)); } } // Copy native libraries to Java folder for (File nativeLibrary : nativeLibraries) { copy(nativeLibrary, new File(javaDirectory, nativeLibrary.getName())); } // Copy icon to Resources folder if (icon == null) { copy(getClass().getResource(DEFAULT_ICON_NAME), new File(resourcesDirectory, DEFAULT_ICON_NAME)); } else { copy(icon, new File(resourcesDirectory, icon.getName())); } } catch (IOException exception) { throw new BuildException(exception); } } private void writeInfoPlist(File file) throws IOException { Writer out = new BufferedWriter(new FileWriter(file)); XMLOutputFactory output = XMLOutputFactory.newInstance(); try { XMLStreamWriter xout = output.createXMLStreamWriter(out); // Write XML declaration xout.writeStartDocument(); xout.writeCharacters("\n"); // Write plist DTD declaration xout.writeDTD(PLIST_DTD); xout.writeCharacters("\n"); // Begin root element xout.writeStartElement(PLIST_TAG); xout.writeAttribute(PLIST_VERSION_ATTRIBUTE, "1.0"); xout.writeCharacters("\n"); // Begin root dictionary xout.writeStartElement(DICT_TAG); xout.writeCharacters("\n"); // Write bundle properties writeProperty(xout, "CFBundleDevelopmentRegion", "English"); writeProperty(xout, "CFBundleExecutable", EXECUTABLE_NAME); writeProperty(xout, "CFBundleIconFile", (icon == null) ? DEFAULT_ICON_NAME : icon.getName()); writeProperty(xout, "CFBundleIdentifier", identifier); writeProperty(xout, "CFBundleDisplayName", displayName); writeProperty(xout, "CFBundleInfoDictionaryVersion", "6.0"); writeProperty(xout, "CFBundleName", name); writeProperty(xout, "CFBundlePackageType", OS_TYPE_CODE); writeProperty(xout, "CFBundleShortVersionString", shortVersion); writeProperty(xout, "CFBundleSignature", signature); writeProperty(xout, "CFBundleVersion", "1"); writeProperty(xout, "NSHumanReadableCopyright", copyright); // Start Java properties writeKey(xout, "JavaVM"); xout.writeStartElement(DICT_TAG); // Write runtime writeProperty(xout, "Runtime", runtime.getName()); // Write main class name writeProperty(xout, "MainClassName", mainClassName); // Write options writeKey(xout, "Options"); xout.writeStartElement(ARRAY_TAG); xout.writeCharacters("\n"); for (String option : options) { writeString(xout, option); } xout.writeEndElement(); xout.writeCharacters("\n"); // Write arguments writeKey(xout, "Arguments"); xout.writeStartElement(ARRAY_TAG); xout.writeCharacters("\n"); for (String argument : arguments) { writeString(xout, argument); } xout.writeEndElement(); xout.writeCharacters("\n"); // End Java properties xout.writeEndElement(); xout.writeCharacters("\n"); // End root dictionary xout.writeEndElement(); xout.writeCharacters("\n"); // End root element xout.writeEndElement(); xout.writeCharacters("\n"); // Close document xout.writeEndDocument(); xout.writeCharacters("\n"); out.flush(); } catch (XMLStreamException exception) { throw new IOException(exception); } finally { out.close(); } } private void writeKey(XMLStreamWriter xout, String key) throws XMLStreamException { xout.writeStartElement(KEY_TAG); xout.writeCharacters(key); xout.writeEndElement(); xout.writeCharacters("\n"); } private void writeString(XMLStreamWriter xout, String value) throws XMLStreamException { xout.writeStartElement(STRING_TAG); xout.writeCharacters(value); xout.writeEndElement(); xout.writeCharacters("\n"); } private void writeProperty(XMLStreamWriter xout, String key, String value) throws XMLStreamException { writeKey(xout, key); writeString(xout, value); } 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(); 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)); } } } }