/******************************************************************************* * Copyright 2014 See AUTHORS file. * * 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.badlogicgames.packr; import com.lexicalscope.jewel.cli.*; import org.zeroturnaround.zip.ZipUtil; import org.zeroturnaround.zip.commons.IOUtils; import java.io.*; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.function.Predicate; /** * Takes a couple of parameters and a JRE and bundles them into a platform specific * distributable (zip on Windows and Linux, app bundle on Mac OS X). * * @author badlogic */ public class Packr { private PackrConfig config; private Predicate<File> removePlatformLibsFileFilter = f -> false; /** * Install application-side file filter to specify which (additional) files can be * deleted during the removePlatformLibs phase. * <p> * This filter is checked first, before evaluating the "--removelibs" and "--libs" options. * * @return true if file should be removed (deleted) */ public Packr setRemovePlatformLibsFileFilter(Predicate<File> filter) { removePlatformLibsFileFilter = filter; return this; } @SuppressWarnings("WeakerAccess") public void pack(PackrConfig config) throws IOException { config.validate(); this.config = config; PackrOutput output = new PackrOutput(config.outDir, config.outDir); cleanOrCreateOutputFolder(output); output = buildMacBundle(output); copyExecutableAndClasspath(output); writeConfig(output); copyAndMinimizeJRE(output, config); copyResources(output); PackrReduce.removePlatformLibs(output, config, removePlatformLibsFileFilter); System.out.println("Done!"); } private void cleanOrCreateOutputFolder(PackrOutput output) throws IOException { File folder = output.executableFolder; if (folder.exists()) { System.out.println("Cleaning output directory '" + folder.getAbsolutePath() + "' ..."); PackrFileUtils.deleteDirectory(folder); } PackrFileUtils.mkdirs(folder); } private PackrOutput buildMacBundle(PackrOutput output) throws IOException { if (config.platform != PackrConfig.Platform.MacOS) { return output; } // replacement strings for Info.plist Map<String, String> values = new HashMap<>(); values.put("${executable}", config.executable); if (config.bundleIdentifier != null) { values.put("${bundleIdentifier}", config.bundleIdentifier); } else { values.put("${bundleIdentifier}", config.mainClass.substring(0, config.mainClass.lastIndexOf('.'))); } // create folder structure File root = output.executableFolder; PackrFileUtils.mkdirs(new File(root, "Contents")); try (FileWriter info = new FileWriter(new File(root, "Contents/Info.plist"))) { String plist = readResourceAsString("/Info.plist", values); info.write(plist); } File target = new File(root, "Contents/MacOS"); PackrFileUtils.mkdirs(target); File resources = new File(root, "Contents/Resources"); PackrFileUtils.mkdirs(resources); if (config.iconResource != null) { // copy icon to Contents/Resources/icons.icns if (config.iconResource.exists()) { PackrFileUtils.copyFile(config.iconResource, new File(resources, "icons.icns")); } } return new PackrOutput(target, resources); } private void copyExecutableAndClasspath(PackrOutput output) throws IOException { byte[] exe = null; String extension = ""; switch (config.platform) { case Windows32: exe = readResource("/packr-windows.exe"); extension = ".exe"; break; case Windows64: exe = readResource("/packr-windows-x64.exe"); extension = ".exe"; break; case Linux32: exe = readResource("/packr-linux"); break; case Linux64: exe = readResource("/packr-linux-x64"); break; case MacOS: exe = readResource("/packr-mac"); break; } System.out.println("Copying executable ..."); try (OutputStream writer = new FileOutputStream( new File(output.executableFolder, config.executable + extension))) { writer.write(exe); } PackrFileUtils.chmodX(new File(output.executableFolder, config.executable + extension)); System.out.println("Copying classpath(s) ..."); for (String file : config.classpath) { File cpSrc = new File(file); File cpDst = new File(output.resourcesFolder, new File(file).getName()); if (cpSrc.isFile()) { PackrFileUtils.copyFile(cpSrc, cpDst); } else if (cpSrc.isDirectory()) { PackrFileUtils.copyDirectory(cpSrc, cpDst); } else { System.err.println("Warning! Classpath not found: " + cpSrc); } } } private void writeConfig(PackrOutput output) throws IOException { StringBuilder builder = new StringBuilder(); builder.append("{\n"); builder.append(" \"classPath\": ["); String delim = "\n"; for (String f : config.classpath) { builder.append(delim).append(" \"").append(new File(f).getName()).append("\""); delim = ",\n"; } builder.append("\n ],\n"); builder.append(" \"mainClass\": \"").append(config.mainClass).append("\",\n"); builder.append(" \"vmArgs\": [\n"); for (int i = 0; i < config.vmArgs.size(); i++) { String vmArg = config.vmArgs.get(i); builder.append(" \""); if (!vmArg.startsWith("-")) { builder.append("-"); } builder.append(vmArg).append("\""); if (i < config.vmArgs.size() - 1) { builder.append(","); } builder.append("\n"); } builder.append(" ]\n"); builder.append("}"); try (Writer writer = new FileWriter(new File(output.resourcesFolder, "config.json"))) { writer.write(builder.toString()); } } private void copyAndMinimizeJRE(PackrOutput output, PackrConfig config) throws IOException { boolean extractToCache = config.cacheJre != null; boolean skipExtractToCache = false; // check if JRE extraction (and minimize) can be skipped if (extractToCache && config.cacheJre.exists()) { if (config.cacheJre.isDirectory()) { // check if the cache directory is empty String[] files = config.cacheJre.list(); skipExtractToCache = files != null && files.length > 0; } else { throw new IOException(config.cacheJre + " must be a directory"); } } // path to extract JRE to (cache, or target folder) File jreStoragePath = extractToCache ? config.cacheJre : output.resourcesFolder; if (skipExtractToCache) { System.out.println("Using cached JRE in '" + config.cacheJre + "' ..."); } else { // path to extract JRE from (folder, zip or remote) boolean fetchFromRemote = config.jdk.startsWith("http://") || config.jdk.startsWith("https://"); File jdkFile = fetchFromRemote ? new File(jreStoragePath, "jdk.zip") : new File(config.jdk); // download from remote if (fetchFromRemote) { System.out.println("Downloading JDK from '" + config.jdk + "' ..."); try (InputStream remote = new URL(config.jdk).openStream()) { try (OutputStream outJdk = new FileOutputStream(jdkFile)) { IOUtils.copy(remote, outJdk); } } } // unpack JDK zip (or copy if it's a folder) System.out.println("Unpacking JRE ..."); File tmp = new File(jreStoragePath, "tmp"); if (tmp.exists()) { PackrFileUtils.deleteDirectory(tmp); } PackrFileUtils.mkdirs(tmp); if (jdkFile.isDirectory()) { PackrFileUtils.copyDirectory(jdkFile, tmp); } else { ZipUtil.unpack(jdkFile, tmp); } // copy the JRE sub folder File jre = searchJre(tmp); if (jre == null) { throw new IOException("Couldn't find JRE in JDK, see '" + tmp.getAbsolutePath() + "'"); } PackrFileUtils.copyDirectory(jre, new File(jreStoragePath, "jre")); PackrFileUtils.deleteDirectory(tmp); if (fetchFromRemote) { PackrFileUtils.delete(jdkFile); } // run minimize PackrReduce.minimizeJre(jreStoragePath, config); } if (extractToCache) { // if cache is used, copy again here; if the JRE is cached already, // this is the only copy done (and everything above is skipped) PackrFileUtils.copyDirectory(jreStoragePath, output.resourcesFolder); } } private File searchJre(File tmp) { if (tmp.getName().equals("jre") && tmp.isDirectory() && (new File(tmp, "bin/java").exists() || new File(tmp, "bin/java.exe").exists())) { return tmp; } File[] childs = tmp.listFiles(); if (childs != null) { for (File child : childs) { if (child.isDirectory()) { File found = searchJre(child); if (found != null) { return found; } } } } return null; } private void copyResources(PackrOutput output) throws IOException { if (config.resources != null) { System.out.println("Copying resources ..."); for (File file : config.resources) { if (!file.exists()) { throw new IOException("Resource '" + file.getAbsolutePath() + "' doesn't exist"); } if (file.isFile()) { PackrFileUtils.copyFile(file, new File(output.resourcesFolder, file.getName())); } if (file.isDirectory()) { File target = new File(output.resourcesFolder, file.getName()); PackrFileUtils.mkdirs(target); PackrFileUtils.copyDirectory(file, target); } } } } private byte[] readResource(String resource) throws IOException { return IOUtils.toByteArray(Packr.class.getResourceAsStream(resource)); } private String readResourceAsString(String resource, Map<String, String> values) throws IOException { String txt = IOUtils.toString(Packr.class.getResourceAsStream(resource), "UTF-8"); return replace(txt, values); } private String replace(String txt, Map<String, String> values) { for (String key : values.keySet()) { String value = values.get(key); txt = txt.replace(key, value); } return txt; } public static void main(String[] args) { try { PackrCommandLine commandLine = CliFactory.parseArguments( PackrCommandLine.class, args.length > 0 ? args : new String[] { "-h" }); if (commandLine.help()) { return; } new Packr().pack(new PackrConfig(commandLine)); } catch (ArgumentValidationException e) { for (ValidationFailure failure : e.getValidationFailures()) { System.err.println(failure.getMessage()); } System.exit(-1); } catch (IOException e) { e.printStackTrace(); System.exit(-1); } } }