All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.badlogicgames.packr.Packr Maven / Gradle / Ivy

/*
 * Copyright 2020 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.ArgumentValidationException;
import com.lexicalscope.jewel.cli.CliFactory;
import com.lexicalscope.jewel.cli.ValidationFailure;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.compressors.CompressorException;
import org.apache.commons.compress.utils.IOUtils;

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.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;

import static com.badlogicgames.packr.ArchiveUtils.extractArchive;

/**
 * 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 removePlatformLibsFileFilter = f -> false;

   /**
    * The main CLI entrance.
    *
    * @param args Should conform to {@link PackrCommandLine}
    */
   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 argumentException) {
         for (ValidationFailure failure : argumentException.getValidationFailures()) {
            System.err.println(failure.getMessage());
         }
         System.exit(-1);
      } catch (IOException | CompressorException | ArchiveException exception) {
         exception.printStackTrace();
         System.exit(-1);
      }
   }

   /**
    * Reads a classpath resource and loads it into a byte array.
    *
    * @param resource the resource to load from the classpath relative to {@link Packr#getClass()}. Use a leading "/" to not load relative to the Packr
    *       package "com/badlogicgames/packr"
    *
    * @return the byte array containing the contents of the resource
    *
    * @throws IOException if an IO error occurs
    */
   private static byte[] readResource(String resource) throws IOException {
      try (InputStream inputStream = Packr.class.getResourceAsStream(resource)) {
         if (inputStream == null) {
            throw new IllegalArgumentException("Couldn't find resource " + resource + " relative to class " + Packr.class.getName());
         }
         return IOUtils.toByteArray(inputStream);
      }
   }

   /**
    * Loads a resource relative to this package and replaces the keys in {@code value} with their values.
    *
    * @param resource the resource to load from the classpath
    * @param values the values to replace
    *
    * @return the resource content loaded and replaces with {@code values}
    *
    * @throws IOException if an IO error occurs
    */
   private static String readResourceAsString(@SuppressWarnings("SameParameterValue") String resource, Map values) throws IOException {
      return replace(new String(readResource(resource), StandardCharsets.UTF_8), values);
   }

   /**
    * Replaces every occurrence of {@code values} key with it's value in the map.
    *
    * @param txt the text to replace values in
    * @param values the mapping of values to replace
    *
    * @return a new String with all the keys in {@code values} replaced with their map value
    */
   private static String replace(String txt, Map values) {
      for (String key : values.keySet()) {
         String value = values.get(key);
         txt = txt.replace(key, value);
      }
      return txt;
   }

   /**
    * Install application-side file filter to specify which (additional) files can be deleted during the removePlatformLibs phase.
    * 

* This filter is checked first, before evaluating the "--removelibs" and "--libs" options. * * @param filter file filter for removing libraries * * @return true if file should be removed (deleted) */ @SuppressWarnings("unused") public Packr setRemovePlatformLibsFileFilter(Predicate filter) { removePlatformLibsFileFilter = filter; return this; } /** * Process all inputs from {@code config} and create an output bundle in {@link PackrConfig#outDir}. * * @param config the configuration information for creating an executable and asset bundle * * @throws IOException if an IO error occurs * @throws CompressorException if a compression error occurs * @throws ArchiveException if an archive error occurs */ @SuppressWarnings("WeakerAccess") public void pack(PackrConfig config) throws IOException, CompressorException, ArchiveException { config.validate(); this.config = config; PackrOutput output = new PackrOutput(config.outDir, config.outDir); cleanAndCreateOutputFolder(output); output = buildMacBundle(output); copyExecutableAndClasspath(output); writeConfig(output); copyAndMinimizeJRE(output, config); copyResources(output); PackrReduce.removePlatformLibs(output, config, removePlatformLibsFileFilter); System.out.println("Done!"); } /** * If the output directory already exists, delete it. Then create the directory. * * @param output Deletes if it exists and then creates {@link PackrOutput#executableFolder} * * @throws IOException if an IO error occurs */ private void cleanAndCreateOutputFolder(PackrOutput output) throws IOException { File folder = output.executableFolder; if (folder.exists()) { System.out.println("Cleaning output directory '" + folder.getAbsolutePath() + "' ..."); PackrFileUtils.deleteDirectory(folder); } Files.createDirectories(folder.toPath()); } /** * Create a bundle for the macOS platform. * * @param output the output location for the bundle * * @return the output paths for the bundle * * @throws IOException if an IO error occurs */ private PackrOutput buildMacBundle(PackrOutput output) throws IOException { if (config.platform != PackrConfig.Platform.MacOS) { return output; } // replacement strings for Info.plist Map 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; Files.createDirectories(root.toPath().resolve("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"); Files.createDirectories(target.toPath()); File resources = new File(root, "Contents/Resources"); Files.createDirectories(resources.toPath()); if (config.iconResource != null) { // copy icon to Contents/Resources/icons.icns if (config.iconResource.exists()) { Files.copy(config.iconResource.toPath(), resources.toPath().resolve("icons.icns"), StandardCopyOption.COPY_ATTRIBUTES); } } return new PackrOutput(target, resources); } /** * Copy the packr launcher executable and classpath files into the bundle. * * @param output the directory to copy the executable and classpath entries into * * @throws IOException if an IO error occurs */ private void copyExecutableAndClasspath(PackrOutput output) throws IOException { byte[] exe = null; String extension = ""; switch (config.platform) { case Windows64: exe = readResource("/packr-windows-x64.exe"); extension = ".exe"; break; case Linux64: exe = readResource("/packr-linux-x64"); break; case MacOS: exe = readResource("/packr-mac"); break; } System.out.println("Copying executable ..."); Files.write(output.executableFolder.toPath().resolve(config.executable + extension), 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()) { Files.copy(cpSrc.toPath(), cpDst.toPath(), StandardCopyOption.COPY_ATTRIBUTES); } else if (cpSrc.isDirectory()) { PackrFileUtils.copyDirectory(cpSrc, cpDst); } else { System.err.println("Warning! Classpath not found: " + cpSrc); } } } /** * Writes a configuration file for the Packr launcher. * * @param output the location to write the configuration file * * @throws IOException if an IO error occurs */ private void writeConfig(PackrOutput output) throws IOException { StringBuilder builder = new StringBuilder(); builder.append("{\n"); builder.append(" \"classPath\": ["); String delimiter = "\n"; for (String f : config.classpath) { builder.append(delimiter).append(" \"").append(new File(f).getName()).append("\""); delimiter = ",\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()); } } /** * Acquires the JDK specified and unpacks it if it's not a directory into a new temporary directory. The new temporary directory for the JDK is minimized. * * @param output the output for the minimized JDK * @param config the packr config for locating the JDK * * @throws IOException if an IO error occurs * @throws CompressorException if a compression error occurs * @throws ArchiveException if an archive error occurs */ private void copyAndMinimizeJRE(PackrOutput output, PackrConfig config) throws IOException, CompressorException, ArchiveException { 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); } Files.createDirectories(tmp.toPath()); if (jdkFile.isDirectory()) { PackrFileUtils.copyDirectory(jdkFile, tmp); } else { extractArchive(jdkFile.toPath(), tmp.toPath()); } // copy the JVM sub folder File jre = findJvmDynamicLibraryBaseDirectory(tmp.toPath()); 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) { Files.deleteIfExists(jdkFile.toPath()); } // 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); } } /** * Searches the directory {@code tmp} for the JVM shared library (jvm.dll, libjvm.so, or libjvm.dylib) and returns the root directory holding the bin and lib * directories. * * @param directoryToSearch the directory to search for the base directory containing the JVM shared library and bin and lib directories * * @return tmp the base directory containing the JVM files that Packr Launcher uses to create a JVM for the application * * @throws IOException if an IO error occurs */ private File findJvmDynamicLibraryBaseDirectory(Path directoryToSearch) throws IOException { final Path[] jvmBaseDirectory = {null}; Files.walkFileTree(directoryToSearch, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { final String filename = file.getFileName().toString(); if (filename.equalsIgnoreCase("jvm.dll") || filename.startsWith("libjvm")) { getParentLibOrBinDirectoryParent(file); return FileVisitResult.TERMINATE; } return FileVisitResult.CONTINUE; } /** * Walks backwards searching for a "lib" or "bin" directory that should be in the base directory for a JVM that needs to be used to launch Java * applications with Packr Launcher. * @param jvmSharedLibrary the path to the JVM shared library * * @throws IOException if an IO error occurs */ private void getParentLibOrBinDirectoryParent(Path jvmSharedLibrary) throws IOException { Path parentDirectory = jvmSharedLibrary.getParent(); while (parentDirectory != null && !Files.isSameFile(directoryToSearch, parentDirectory)) { final String parentDirectoryName = parentDirectory.getFileName().toString(); if (parentDirectoryName.equalsIgnoreCase("lib") || parentDirectoryName.equalsIgnoreCase("bin")) { jvmBaseDirectory[0] = parentDirectory.getParent(); break; } parentDirectory = parentDirectory.getParent(); } } }); return jvmBaseDirectory[0] == null ? null : jvmBaseDirectory[0].toFile(); } /** * Copies the specified bundle resources into the bundle. * * @param output the resource output folder to copy into * * @throws IOException if an IO error occurs */ 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()) { Files.copy(file.toPath(), output.resourcesFolder.toPath().resolve(file.getName()), StandardCopyOption.COPY_ATTRIBUTES); } if (file.isDirectory()) { File target = new File(output.resourcesFolder, file.getName()); Files.createDirectories(target.toPath()); PackrFileUtils.copyDirectory(file, target); } } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy